用法
Jimmer分页的特色
分页查询是Jimmer一个很有特色的功能,能大幅提升开发效率。
分页需要执行两条SQL查询
-
查询满足条件的数据总行数,其结果可以计算一共有多少页,用户的页码是否越界。
信息为了便于讨论,Jimmer称这条SQL为
count-query
-
查询当前页内的所有数据,返回的数据条数不超过页面大小,并跳过之前页的所有数据。
信息为了便于讨论,Jimmer称这条SQL为
data-query
Jimmer分页的特色:开发人员只需编写data-query
,框架自动生成count-query
。
Jimmer不但可以自动生成count-query
,还可以做到优化count-query
。这个优化会在下一篇文章中讨论。
配合Spring Data使用时
配合SpringBoot使用时,开发人员从JRepository/KRepository
派 生自定义的Repository接口,为自定义接口添加查询方法有两种选择:
-
按照一定的约定声明抽象方法,交由Jimmer自动实现
警告这种用法过于简单,隐藏了所有细节,不适在此讲述分页查询。
你可以查看Spring篇/SpringData风格/抽象方法以了解如何通过这种方式实现分页查询
-
直接在自定义接口中定义default方法,自己实现查询逻辑
- Java
- Kotlin
import org.springframework.data.domain.Page;
import org.springframework.data.domain.Pageable;
org.babyfish.jimmer.spring.repository.support.SpringPageFactory;
...省略其他导入...
public interface BookRepository<Book, Long> extends JRepository<Book, Long> {
BookTable table = Tables.BOOK_TABLE;
default Page<Book> findBooks(
Pageable pageable,
@Nullable String name,
@Nullable String storeName
) {
return
sql()
.createQuery(table)
.whereIf(
name != null && !name.isEmpty(),
table.name().eq(name)
)
.whereIf(
storeName != null && !storeName.isEmpty(),
table.store().name().eq(storeName)
)
.orderBy(SpringOrders.toOrders(table, pageable.getSort())) ❶
.select(table)
.fetchPage( ❷
pageable.getPageNumber(),
pageable.getPageSize()
SpringPageFactory.getInstance()
);
}
}
import org.springframework.data.domain.Page
import org.springframework.data.domain.Pageable
...省略其他导入...
interface BookRepository<Book, Long> : KRepository<Book, Long> {
fun findBooks(
pageable: Pageable,
name: String? = null,
storeName: String? = null
): Page<Book> =
sql
.createQuery(Book::class) {
name?.takeIf { it.isNotEmpty() }?.let {
where(table.name eq it)
}
storeName?.takeIf { it.isNotEmpty() }?.let {
where(table.store.name eq it)
}
orderBy(pageable.sort) ❶
select(table)
}
.fetchSpringPage(pageable) ❷
}
-
❶ 由于Spring Data的
Pageable
包含了动态排序,所以需要应用动态排序。 -
❷ 分页查询,返回
org.springframework.data.domain.Page<Book>
类型的对象Jimmer分页可以使用任何
Page
对象,无论是Spring Data的Page
,还是Jimmer自身的Page
,甚至第三方定义的Page
。这里,Java代码采用
SpringPageFactory.getInstance()
要求当前分页操作返回Spring Data的Page
。事实上,Kotlin代码也可以使用
SpringPageFactory.getInstance()
来时想通目的,但是,kotlin下有更便捷的扩展方法fetchSpringPage
。
如果我们执行
Page<Boo> page = bookRepository.findBooks(
PageRequest.of(
1,
5,
SortUtils.toSort("name asc, edition desc")
),
null,
null
)
SpringData中Pageable
的页码从0开始,而非从1开始,所以,这里查询的是第二页
会生成两条SQL
-
count-query
select
count(tb_1_.ID)
from BOOK tb_1_ -
data-query (假设数据库是H2)
select
tb_1_.ID,
tb_1_.CREATED_TIME,
tb_1_.MODIFIED_TIME,
tb_1_.NAME,
tb_1_.EDITION,
tb_1_.PRICE,
tb_1_.STORE_ID
from BOOK tb_1_
order by
tb_1_.NAME asc,
tb_1_.EDITION desc
limit ? /* 5 */ offset ? /* 5 */
这个例子可以让我们明白Jimmer分页功能,但是,Jimmer的Spring API隐藏了一些细节。因此,接下来,我们绕开Spring Data,从更底层的角度更清晰地阐述。
不配合Spring Data使用时
Jimmer的Page对象
既然不使用Spring Data,那么自然无法使用org.springframework.data.domain.Page<T>
。
为此,Jimmer定义了org.babyfish.jimmer.Page<T>
类型,定义如下
package org.babyfish.jimmer;
public class Page<T> {
private final List<T> rows;
private final int totalRowCount;
private final int totalPageCount;
...省略其他字段...
}
可以看出,Jimmer自身的Page<T>
比Spring Data的Page<T>
简单得多,二者区别如下
-
org.springframework.data.domain.Page<T>
为服务端页面而设计,为了让页面在刷新后仍然能保持之前的状态,大量的信息 (比如繁琐的排序信息) 需要被原样返回给客户端,所以非常复杂。 -
org.babyfish.jimmer.Page<T>
为前后端独立页面的设计,这种客户端页面自身是一个有状态应用,服务端提供纯数据服务即可,仅返回最必要的信息即可,所以非常简单。
实现业务代码
- Java
- Kotlin
public Page<Book> findBooks(
int pageIndex,
int pageSize,
@Nullable String name,
@Nullable String storeName
// such as "price desc, name asc"
@Nullable String orderCode
) {
return sqlClient
.createQuery(table)
.whereIf(
name != null && !name.isEmpty(),
table.name().eq(name)
)
.whereIf(
storeName != null && !storeName.isEmpty(),
table.store().name().eq(storeName)
)
.orderBy(Order.makeOrders(table, orderCode))
.select(table)
.fetchPage();
}
fun findBooks(
pageIndex: Int,
pageSize: Int,
name: String? = null,
storeName: String? = null,
// such as "price desc, name asc"
orderCode: String? = null
): Page<Book> =
sql
.createQuery(Book::class) {
name?.takeIf { it.isNotEmpty() }?.let {
where(table.name eq it)
}
storeName?.takeIf { it.isNotEmpty() }?.let {
where(table.store.name eq it)
}
orderBy(table.makeOrders(orderCode))
select(table)
}
.fetchPage()
最终生成的SQL和前面讨论过的那个给予Spring Data的例子相同,这里不再赘述。
内部机制
在上面的例子中,我们讨论了Java和Kotlin的语言差异,以及是否使用SpringData 的各种情况。
这些行为的底层逻辑是一样的,仅以Java为例
BookTable table = Tables.BOOK_TABLE;
ConfigurableRootQuery<Book> query = ❶
sqlClient
.createQuery(table)
.whereIf(
name != null && !name.isEmpty(),
table.name().eq(name)
)
.whereIf(
storeName != null && !storeName.isEmpty(),
table.store().name().eq(storeName)
)
.orderBy(table.name().asc(), table.edition().desc)
int totalCount = query.fetchUnlimitedCount(); ❷
int totalPage = (totalCount + pageSize - 1) / pageSize;
if (pageIndex >= totalPage) {
return new Page<Book>(totalCount, totalPage, Collections.emptyList());
}
List<Book> entities = query
.limit(pageSize, pageIndex * pageSize) ❸
.execute(); ❹
return new Page( ❺
entities,
totalCount,
totalPage
)
为了简化讨论,这段伪码并未考虑反排序优化
-
❶ 创建查询,但并 不执行。我可以称其为样板查询
-
❷ 在不修改原样板查询的基础上,生成
count-query
,再执行count-query
得到分页前所有数据的行数这里的
fetchUnlimitedCount
方法是快捷API,其底层逻辑为public interface ConfigurableRootQuery<T extends Table<?>, R> extends ... {
default int fetchUnlimitedCount() {
return count(null);
}
default int fetchUnlimitedCount(Connection con) {
return reselect((q, t) -> q.select(t.count()))
.withoutSortingAndPaging()
.execute(con)
.get(0)
.intValue();
}
}-
reselect((q, t) -> q.select(t.count()))
:count-query
并不查询数据,而是查询COUNT -
withoutSortingAndPaging()
:count-query
无需排序子句order by
和分页子句 (比如H2的limit ? offset ?
)
提示Jimmer不但能自动生成
count-query
,还能自动优化count-query
,请参见表连接优化 -
-
❸
limit(limit, offset)
: 在不修改原样板查询的基础上,生成真正的data-query
,带上分页限制。 -
❹ 执行❸生成的
data-query
,得到一页内的数据 -
❺ 将❷和❹取得的数据组合成page对象并返回