分页安全性
分页安全性是为分页设计的功能,但是表连接功能为其提供底层支持。
集合关联的问题
这里,我们先介绍两种关联属性,引用关联和集合关联。
- 被
@OneToOne
或@ManyToOne
修饰的关联属性,称为引用关联 - 被
@OneToMany
或@ManyToMany
修饰的关联属性,称为集合关联
请参考例子,
集合JOIN导致重复查询结果
如果我们把当前被查询的第一张表所对应的实体对象称为聚合根对象,引用关联和集合关联之间有如下差异:
-
查询一个对象,通过引用关联进行table join,在SQL查询结果中,当前主对象不会出现重复记录。例如:
select
b.id as root_id,
b.name as root_name,
s.id as associated_id,
s.name as associated_name
from book b
inner join book_store s
on b.store_id = s.id
where b.id = 1;查询结果如下,聚合根对象没有出现重复
root_id root_name associated_id associated_name 1 Learning GraphQL 1 O'REILLY -
查询一个对象,通过集合关联进行table join,在SQL查询结果中,当前主对象会出现重复记录:
select
b.id as root_id,
b.name as root_name,
a.id as associated_id,
a.first_name as associated_first_name,
a.last_name as associated_last_name
from book b
inner join book_author_mapping m
on b.id = m.book_id
inner join author a
on m.author_id = a.id
where b.id = 1;查询结果如下,聚合根对象有可能出现重复:
root_id root_name associated_id associated_first_name associated_last_name 1 Learning GraphQL 1 Eve Procello 1 Learning GraphQL 2 Alex Banks
查询结果重复的危害
基于集合关联的table join会导致重复数据,进而引发以下问题:
-
如果开发者忘记去重,就会引入bug。
-
即便开发者没有忘记去重,在客户端使用
java.util.LinkedHashSet
进行去重,也并非一个好的选择。 因为事后的去重操作也不能改变数据库返回的原始结果包含重复数据既定事实,且会消耗额外的网络传输和JVM处理数据的成本。 -
最重要的一点,对分页查询不友好。
在SQL层面对table join连接结果进行分页,往往不是人们所需;更多的场景下,人们希望分页操作被应用在聚合根对象上。
以Hibernate为例,在这种情况下,Hibernate不得不放弃SQL层面的分页策略,而采用内存层面的分页策略。这样做性能非常低下,为了引起开发人员的重视,Hibernate会在日志中进行告警。如果读者你有Hibernate使用经验,相信下面这条日志会让你感到非常头疼。
firstResult/maxResults specified with collection fetch; applying in memory
提示注意,Jimmer的另外一个功能对象抓取器,并不是基于table join来实现对集合关联属性进行fetch的,故而无此问题,请放心使用。
由于SQL支持子查询,我们把最外层的查询称为顶层查询。综上,在顶级查询中使用集合连接的缺点非常明显,但不可否认,在子查询中使用集合连接仍然有客观的价值。
所以,Jimmer的SQL DSL有这样的特色
- 基于集合关联的table join,在顶级查询中需要被禁止。
- 基于集合关联的table join,在子查询、update语句和delete语句中仍然可用。
隐含子查询
对于集合关联而言,其实不推荐JOIN,更推荐隐式子查询。
如果仍然要对集合关联属性进行JOIN,请继续阅读下文。
Table和TableEx
在Jimmer的SQL DSL中,存在两种表对象,Table<E>
和TableEx<E>
。
-
Table
特点:只能通过引用关联进行join,无法通过集合关联属性进行join
Java类型:org.babyfish.jimmer.sql.ast.table.Table<E>
Kotlin类型:org.babyfish.jimmer.sql.kt.ast.table.KTable<E>
-
TableEx
特点:可以通过任何关联属性进行join
Java类型:org.babyfish.jimmer.sql.ast.table.TableEx<E>
Kotlin类型:org.babyfish.jimmer.sql.kt.ast.table.KTableEx<E>
生成的代码
为了实现强类型的SQL DSL,Jimmer使用Annotation processor(Java)或KSP(kotlin),根据用户定义的实体接口生成SQL DSL相关的源代码。
还是以文章开头的Book实体接口为例,如下两个类型会被自动生成
- Java
- Kotlin
/*
* BookTable.java
*/
package org.babyfish.jimmer.sql.example.model;
import java.lang.Integer;
import java.math.BigDecimal;
import javax.persistence.criteria.JoinType;
import org.babyfish.jimmer.sql.ast.Expression;
import org.babyfish.jimmer.sql.ast.PropExpression;
import org.babyfish.jimmer.sql.ast.table.Table;
import org.babyfish.jimmer.sql.ast.table.spi.AbstractTableWrapper;
public class BookTable extends AbstractTableWrapper<Book> {
public BookTable(Table<Book> table) {
super(table);
}
public Expression<Long> id() {
return get("id");
}
public PropExpression.Str name() {
return get("name");
}
public PropExpression.Num<Integer> edition() {
return get("edition");
}
public PropExpression.Num<BigDecimal> price() {
return get("price");
}
public BookStoreTable store() {
return join("store");
}
public BookStoreTable store(JoinType joinType) {
return join("store", joinType);
}
}
/*
* BookTableEx.java
*/
package org.babyfish.jimmer.sql.example.model;
import javax.persistence.criteria.JoinType;
import org.babyfish.jimmer.sql.ast.table.TableEx;
public class BookTableEx extends BookTable implements TableEx<Book> {
public BookTableEx(TableEx<Book> table) {
super(table);
}
public AuthorTableEx authors() {
return join("authors");
}
public AuthorTableEx authors(JoinType joinType) {
return join("authors", joinType);
}
}
package org.babyfish.jimmer.example.kt.sql.model
import java.math.BigDecimal
import org.babyfish.jimmer.sql.ast.Selection
import org.babyfish.jimmer.sql.kt.ast.expression.KNonNullPropExpression
import org.babyfish.jimmer.sql.kt.ast.expression.KNullablePropExpression
import org.babyfish.jimmer.sql.kt.ast.table.KNonNullTable
import org.babyfish.jimmer.sql.kt.ast.table.KNonNullTableEx
import org.babyfish.jimmer.sql.kt.ast.table.KNullableTable
import org.babyfish.jimmer.sql.kt.ast.table.KNullableTableEx
import org.babyfish.jimmer.sql.kt.ast.table.KTable
import org.babyfish.jimmer.sql.kt.ast.table.KTableEx
/*
* Extension for Table<Book>
*/
public val KTable<Book>.id: KNullablePropExpression<Long>
get() = get("id")
public val KNonNullTable<Book>.id: KNonNullPropExpression<Long>
get() = get("id")
public val KTable<Book>.name: KNullablePropExpression<String>
get() = get("name")
public val KNonNullTable<Book>.name: KNonNullPropExpression<String>
get() = get("name")
public val KTable<Book>.edition: KNullablePropExpression<Int>
get() = get("edition")
public val KNonNullTable<Book>.edition: KNonNullPropExpression<Int>
get() = get("edition")
public val KTable<Book>.price: KNullablePropExpression<BigDecimal>
get() = get("price")
public val KNonNullTable<Book>.price: KNonNullPropExpression<BigDecimal>
get() = get("price")
public val KNullableTable<Book>.store: KNullableTable<BookStore>
get() = join("store")
public val KNonNullTable<Book>.store: KNonNullTable<BookStore>
get() = join("store")
public val KTable<Book>.`store?`: KNullableTable<BookStore>
get() = outerJoin("store")
/*
* Extension for TableEx<Book>
*/
public val KNullableTableEx<Book>.authors: KNullableTableEx<Author>
get() = join("authors")
public val KNonNullTableEx<Book>.authors: KNonNullTableEx<Author>
get() = join("authors")
public val KTableEx<Book>.`authors?`: KNullableTableEx<Author>
get() = outerJoin("authors")
观察这两个自动生成的类型,不难看出
BookTableEx
继承了BookTable
。BookTable
不支持集合关联,但支持普通字段和引用关联(本例中为store
)。BookTableEx
增加了对集合关联的支持(本例中为authors
)。
因此jimmer-sql的API遵循如下的模式
- 顶级查询只能基于
Table
创建。 - 子查询、update语句和delete语句既可基于
Table
创建,也可基于TableEx
创建。
接下来,以顶层查询和子查询为例,做对比性示范。