• 如何在 Spring 或 Spring Boot 中使用键集分页


    介绍

    在本文中,我将向您展示如何在 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

    这是实体数据访问对象,它看起来像这样:PostRepositoryPost

    1
    2
    3
    4
    @Repository
    public interface PostRepository
            extends JpaRepository, CustomPostRepository {
    }

    本文所述,如果我们想提供额外的数据访问方法,我们可以在定义自定义数据访问逻辑的地方进行扩展。PostRepositoryCustomPostRepository

    外观如下:CustomPostRepository

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    public interface CustomPostRepository {
     
        PagedList findTopN(
            Sort sortBy,
            int pageSize
        );
     
        PagedList findNextN(
            Sort orderBy,
            PagedList previousPage
        );
    }

    实现接口的类如下所示:CustomPostRepositoryImplCustomPostRepository

    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 findTopN(
                Sort sortBy,
                int pageSize) {
            return sortedCriteriaBuilder(sortBy)
                .page(0, pageSize)
                .withKeysetExtraction(true)
                .getResultList();
        }
     
        @Override
        public PagedList findNextN(
                Sort sortBy,
                PagedList previousPage) {
            return sortedCriteriaBuilder(sortBy)
                .page(
                    previousPage.getKeysetPage(),
                    previousPage.getPage() * previousPage.getMaxResults(),
                    previousPage.getMaxResults()
                )
                .getResultList();
        }
     
        private CriteriaBuilder sortedCriteriaBuilder(
                Sort sortBy) {
            CriteriaBuilder criteriaBuilder = criteriaBuilderFactory
                .create(entityManager, Post.class);
                 
            sortBy.forEach(order -> {
                criteriaBuilder.orderBy(
                    order.getProperty(),
                    order.isAscending()
                );
            });
             
            return criteriaBuilder;
        }
    }

    使用键集分页方法,如下所示:ForumServicePostRepository

    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 firstLatestPosts(
                int pageSize) {
            return postRepository.findTopN(
                Sort.by(
                    Post_.CREATED_ON
                ).descending().and(
                    Sort.by(
                        Post_.ID
                    ).descending()
                ),
                pageSize
            );
        }
     
        public PagedList findNextLatestPosts(
                PagedList 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(
        2021123012000
    );
     
    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 topPage = forumService.firstLatestPosts(PAGE_SIZE);
     
    assertEquals(POST_COUNT, topPage.getTotalSize());
     
    assertEquals(POST_COUNT / PAGE_SIZE, topPage.getTotalPages());
     
    assertEquals(1, topPage.getPage());
     
    List 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 nextPage = forumService.findNextLatestPosts(topPage);
     
    assertEquals(2, nextPage.getPage());
     
    List 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 数据存储库轻松地自己实现它。

     

  • 相关阅读:
    39-Jenkins-获取构建人信息
    贪心 Leetcode 1005 K次取反后最大化的数组和
    Linux防火墙添加白名单
    Nftea: 世界杯文化、 NFT与 期权的首度碰撞
    Day 50 | 123. 买卖股票的最佳时机 III & 188. 买卖股票的最佳时机 IV
    【深入浅出Docker原理及实战】「原理实战体系」零基础+全方位带你学习探索Docker容器开发实战指南(底层实现系列)
    模型部署时的调试技巧,debug方法
    用C语言将无符号整数转换为字符串
    MySQL学习大纲
    C++ RAII在HotSpot VM中的重要应用
  • 原文地址:https://blog.csdn.net/allway2/article/details/128178450