在本文中,我将向您展示如何在 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 数据存储库类中抽象。JpaRepository
这是实体数据访问对象,它看起来像这样:PostRepository
Post
1 2 3 4 | @Repository public interface PostRepository extends JpaRepository } |
如本文所述,如果我们想提供额外的数据访问方法,我们可以在定义自定义数据访问逻辑的地方进行扩展。PostRepository
CustomPostRepository
外观如下:CustomPostRepository
1 2 3 4 5 6 7 8 9 10 11 12 | public interface CustomPostRepository { PagedList Sort sortBy, int pageSize ); PagedList Sort orderBy, PagedList ); } |
实现接口的类如下所示: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 Sort sortBy, int pageSize) { return sortedCriteriaBuilder(sortBy) .page( 0 , pageSize) .withKeysetExtraction( true ) .getResultList(); } @Override public PagedList Sort sortBy, PagedList return sortedCriteriaBuilder(sortBy) .page( previousPage.getKeysetPage(), previousPage.getPage() * previousPage.getMaxResults(), previousPage.getMaxResults() ) .getResultList(); } private CriteriaBuilder Sort sortBy) { CriteriaBuilder .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 int pageSize) { return postRepository.findTopN( Sort.by( Post_.CREATED_ON ).descending().and( Sort.by( Post_.ID ).descending() ), pageSize ); } public PagedList PagedList 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 assertEquals(POST_COUNT, topPage.getTotalSize()); assertEquals(POST_COUNT / PAGE_SIZE, topPage.getTotalPages()); assertEquals( 1 , topPage.getPage()); List 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 assertEquals( 2 , nextPage.getPage()); List 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 数据存储库轻松地自己实现它。