介绍
在本文中,我将向您展示如何在 Spring 或 Spring Boot 中使用键集分页技术。
虽然 Spring DataPagingAndSortingRepository提供的基于偏移量的默认分页在许多情况下很有用,但如果您必须迭代大型结果集,那么键集分页或查找方法技术可以提供更好的性能。
什么是键集分页
如本文所述,键集分页或查找方法允许我们在查找要加载的给定页面的第一个元素时使用索引。
加载最新 25 个实体的 Top-N 键集分页查询如下所示:Post
1
2
3
4
5
6
7
8
9
10
|
SELECT
id,
title,
created_on
FROM
post
ORDER BY
created_on DESC ,
id DESC
FETCH FIRST 25 ROWS ONLY
|
加载第二、第三或第 n 页的 Next-N 查询如下所示:
1
2
3
4
5
6
7
8
9
10
11
12
13
|
SELECT
id,
title,
created_on
FROM
post
WHERE
(created_on, id) <
(:previousCreatedOn, :previousId)
ORDER BY
created_on DESC ,
id DESC
FETCH FIRST 25 ROWS ONLY
|
如您所见,Keyset 分页查询是特定于数据库的,因此我们需要一个框架,该框架可以为我们提供抽象此功能的 API,同时为每个受支持的关系数据库生成适当的 SQL 查询。
该框架称为Blaze Persistence,它支持JPA实体查询的Keyset Pagination。
如何在 Spring 中使用键集分页
使用 Spring 时,数据访问逻辑是使用 Spring 数据存储库实现的。因此,基本数据访问方法由 定义,并且自定义逻辑可以在一个或多个自定义 Spring 数据存储库类中抽象。JpaRepository
这是实体数据访问对象,它看起来像这样:PostRepository
Post
1
2
3
4
|
@Repository
public interface PostRepository
extends JpaRepository<Post, Long>, CustomPostRepository {
}
|
如本文所述,如果我们想提供额外的数据访问方法,我们可以在定义自定义数据访问逻辑的地方进行扩展。PostRepository
CustomPostRepository
外观如下:CustomPostRepository
1
2
3
4
5
6
7
8
9
10
11
12
|
public interface CustomPostRepository {
PagedList<Post> findTopN(
Sort sortBy,
int pageSize
);
PagedList<Post> findNextN(
Sort orderBy,
PagedList<Post> previousPage
);
}
|
实现接口的类如下所示:CustomPostRepositoryImpl
CustomPostRepository
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
|
public class CustomPostRepositoryImpl
implements CustomPostRepository {
@PersistenceContext
private EntityManager entityManager;
@Autowired
private CriteriaBuilderFactory criteriaBuilderFactory;
@Override
public PagedList<Post> findTopN(
Sort sortBy,
int pageSize) {
return sortedCriteriaBuilder(sortBy)
.page( 0 , pageSize)
.withKeysetExtraction( true )
.getResultList();
}
@Override
public PagedList<Post> findNextN(
Sort sortBy,
PagedList<Post> previousPage) {
return sortedCriteriaBuilder(sortBy)
.page(
previousPage.getKeysetPage(),
previousPage.getPage() * previousPage.getMaxResults(),
previousPage.getMaxResults()
)
.getResultList();
}
private CriteriaBuilder<Post> sortedCriteriaBuilder(
Sort sortBy) {
CriteriaBuilder<Post> criteriaBuilder = criteriaBuilderFactory
.create(entityManager, Post. class );
sortBy.forEach(order -> {
criteriaBuilder.orderBy(
order.getProperty(),
order.isAscending()
);
});
return criteriaBuilder;
}
}
|
使用键集分页方法,如下所示:ForumService
PostRepository
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
|
@Service
@Transactional (readOnly = true )
public class ForumService {
@Autowired
private PostRepository postRepository;
public PagedList<Post> firstLatestPosts(
int pageSize) {
return postRepository.findTopN(
Sort.by(
Post_.CREATED_ON
).descending().and(
Sort.by(
Post_.ID
).descending()
),
pageSize
);
}
public PagedList<Post> findNextLatestPosts(
PagedList<Post> previousPage) {
return postRepository.findNextN(
Sort.by(
Post_.CREATED_ON
).descending().and(
Sort.by(
Post_.ID
).descending()
),
previousPage
);
}
}
|
测试时间
假设我们创建了 50 个实体:Post
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
|
LocalDateTime timestamp = LocalDateTime.of(
2021 , 12 , 30 , 12 , 0 , 0 , 0
);
LongStream.rangeClosed( 1 , POST_COUNT).forEach(postId -> {
Post post = new Post()
.setId(postId)
.setTitle(
String.format(
"High-Performance Java Persistence - Chapter %d" ,
postId
)
)
.setCreatedOn(
Timestamp.valueOf(timestamp.plusMinutes(postId))
);
entityManager.persist(post);
});
|
加载第一页时,我们得到预期的结果:
1
2
3
4
5
6
7
8
9
10
11
12
|
PagedList<Post> topPage = forumService.firstLatestPosts(PAGE_SIZE);
assertEquals(POST_COUNT, topPage.getTotalSize());
assertEquals(POST_COUNT / PAGE_SIZE, topPage.getTotalPages());
assertEquals( 1 , topPage.getPage());
List<Long> topIds = topPage.stream().map(Post::getId).toList();
assertEquals(Long.valueOf( 50 ), topIds.get( 0 ));
assertEquals(Long.valueOf( 49 ), topIds.get( 1 ));
|
而且,在PostgreSQL上执行的SQL查询如下所示:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
|
SELECT
p.id AS col_0_0_,
p.created_on AS col_1_0_,
p.id AS col_2_0_,
(
SELECT count (*)
FROM post post1_
) AS col_3_0_,
p.id AS id1_0_,
p.created_on AS created_2_0_,
p.title AS title3_0_
FROM
post p
ORDER BY
p.created_on DESC ,
p.id DESC
LIMIT 25
|
加载第二页时,我们得到下一个最新的 25 个实体:Post
1
2
3
4
5
6
7
8
|
PagedList<Post> nextPage = forumService.findNextLatestPosts(topPage);
assertEquals( 2 , nextPage.getPage());
List<Long> nextIds = nextPage.stream().map(Post::getId).toList();
assertEquals(Long.valueOf( 25 ), nextIds.get( 0 ));
assertEquals(Long.valueOf( 24 ), nextIds.get( 1 ));
|
底层 SQL 查询如下所示:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
|
SELECT
p.id AS col_0_0_,
p.created_on AS col_1_0_,
p.id AS col_2_0_,
(
SELECT count (*)
FROM post post1_
) AS col_3_0_,
p.id AS id1_0_,
p.created_on AS created_2_0_,
p.title AS title3_0_
FROM
post p
WHERE
(p.created_on, p.id) <
( '2021-12-30 12:26:00.0' , 26) AND 0=0
ORDER BY
p.created_on DESC ,
p.id DESC
LIMIT 25
|
很酷,对吧?
结论
键集分页在实现无限滚动解决方案时非常有用,虽然 Spring Data 中没有内置支持它,但我们可以使用 Blaze Persistence 和自定义 Spring 数据存储库轻松地自己实现它。