【问题标题】:Cursor based pagination for table without a unique and sequential id column?没有唯一和顺序 id 列的表的基于光标的分页?
【发布时间】:2020-06-04 23:37:20
【问题描述】:

我正在研究使用基于光标的分页。我有以下(postgres)表:

CREATE TABLE label (
    tenantId VARCHAR(256) NOT NULL,
    tagFieldName VARCHAR(256) NOT NULL,
    tagId VARCHAR(256) NOT NULL,
    "key" VARCHAR(256) NOT NULL,
    "value" VARCHAR(256) NOT NULL,
    PRIMARY KEY (tenantId, tagFieldName, tagId, "key"),
    CONSTRAINT fkTag FOREIGN KEY (tenantId, tagFieldName, tagId) REFERENCES tag (tenantId, fieldName, id)
);

并且已经开始画出分页查询(未测试):

CriteriaBuilder criteriaBuilder = entityManager
            .getCriteriaBuilder();

CriteriaQuery<Label> criteriaQuery = criteriaBuilder
        .createQuery(Label.class);

Root<Label> root = criteriaQuery.from(Label.class);

final Predicate tenantIdPredicate = criteriaBuilder.equal(root.get("tenantId"), "jim");
final Predicate fieldNamePredicate = criteriaBuilder.equal(root.get("fieldName"), "work");
final Predicate idPredicate = criteriaBuilder.equal(root.get("id"), "work1");

CriteriaQuery<Label> select = criteriaQuery
        .select(root).where(criteriaBuilder.and(tenantIdPredicate, fieldNamePredicate, idPredicate));

criteriaQuery.orderBy(
        criteriaBuilder.asc(root.get("tenantId")),
        criteriaBuilder.asc(root.get("fieldName")),
        criteriaBuilder.asc(root.get("id")),
        criteriaBuilder.asc(root.get("key")));

TypedQuery<Label> typedQuery = entityManager.createQuery(select);

typedQuery.setMaxResults(100);

List<Label> labels = typedQuery.getResultList();

我一直在阅读这些帖子:

https://slack.engineering/evolving-api-pagination-at-slack-1c1f644f8e12

https://engineering.mixmax.com/blog/api-paging-built-the-right-way/

https://coderwall.com/p/lkcaag/pagination-you-re-probably-doing-it-wrong

两者都声明光标应该是一个唯一的、连续的分页列。

但是,我的表没有有一个自动递增的顺序主键。

相反,它的主键是这四个字段的组合键,它们一起是唯一的,但不是顺序的:

PRIMARY KEY (tenantId, tagFieldName, tagId, "key"),

我能否实现基于光标的分页方法?

还可能允许用户指定一个排序列,例如对“值”列进行排序?

更新:根据@Lesiak 的评论,我相信以下查询可能接近我想要的,请记住tenantIdtagFieldNametagId 部分主键是已知的(客户端提供),但主键的 key 字段将未知,假设客户端已要求对 value 字段进行排序。

-- First page
SELECT *
FROM label
WHERE
tenantId = 'jim' AND tagFieldName = 'work' AND tagId = 'work1'
ORDER BY value, tenantId, tagFieldName, tagId, key
LIMIT 2;

-- Next page (assuming the last result of the first query returned a record with the key `label-2-key` and and value `label-2-value`) 
SELECT *
FROM label
WHERE
tenantId = 'jim' AND tagFieldName = 'work' AND tagId = 'work1'
AND
(value, key) > ('label-2-value', 'label-2-key')
ORDER BY value, tenantId, tagFieldName, tagId, key
LIMIT 2;

展开第二个(下一页)查询的行值表达式会导致:

SELECT *
FROM label
WHERE
tenantId = 'jim' AND tagFieldName = 'work' AND tagId = 'work1'
AND
value > 'label-2-value' OR ((value = 'label-2-value') AND (key > 'label-2-key'))
ORDER BY value, tenantId, tagFieldName, tagId, key
LIMIT 2;

这将转化为 JPA 查询,例如(未经测试):

CriteriaBuilder criteriaBuilder = entityManager
            .getCriteriaBuilder();

CriteriaQuery<Label> criteriaQuery = criteriaBuilder
        .createQuery(Label.class);

Root<Label> root = criteriaQuery.from(Label.class);

final Predicate tenantIdEqualPredicate = criteriaBuilder.equal(root.get("tenantId"), tenantId);
final Predicate fieldNameEqualPredicate = criteriaBuilder.equal(root.get("fieldName"), fieldName);
final Predicate idEqualPredicate = criteriaBuilder.equal(root.get("id"), id);

// tenantId = 'jim' AND tagFieldName = 'work' AND tagId = 'work1'
Predicate primaryKeyPredicate = criteriaBuilder.and(tenantIdEqualPredicate, fieldNameEqualPredicate, idEqualPredicate);

final Predicate valueGreaterThanPredicate = criteriaBuilder.greaterThan(root.get("value"), "label-2-value");
final Predicate valueEqualPredicate = criteriaBuilder.equal(root.get("value"), "label-2-value");
final Predicate keyGreaterThanPredicate = criteriaBuilder.equal(root.get("key"), "label-2-key");

// value > 'label-2-value' OR ((value = 'label-2-value') AND (key > 'label-2-key'))
Predicate orderingPredicate = criteriaBuilder.or(
        valueGreaterThanPredicate, criteriaBuilder.and(valueEqualPredicate, keyGreaterThanPredicate));

CriteriaQuery<Label> select = criteriaQuery
        .select(root).where(
                criteriaBuilder.and(primaryKeyPredicate, orderingPredicate)); 

criteriaQuery.orderBy(
        criteriaBuilder.asc(root.get("value")),
        criteriaBuilder.asc(root.get("tenantId")),
        criteriaBuilder.asc(root.get("fieldName")),
        criteriaBuilder.asc(root.get("id")),
        criteriaBuilder.asc(root.get("key")));

TypedQuery<Label> typedQuery = entityManager.createQuery(select);

typedQuery.setMaxResults(Defaults.PAGE_SIZE);

List<Label> labels = typedQuery.getResultList();

我对此的问题/疑虑是:

  • 当用户指定多个 对字段进行排序,其中一些可能按 asc/desc 顺序等?

  • 用户还可以为此请求指定过滤器, 例如“标签存在”、“标签键包含”、“标签键以开头” 等等,以及这将如何再次增加构建的复杂性 条件对象?

【问题讨论】:

    标签: java sql spring jpa pagination


    【解决方案1】:

    是的,您可以使用复合键实现键集分页。但是 JPA 并不是理想的工具——你必须自己构建你的谓词。

    详情:

    让我们修改一个原生支持键集分页的框架的示例: https://www.jooq.org/doc/latest/manual/sql-building/sql-statements/select-statement/seek-clause/

    SELECT id1, id2, value
    FROM t
    WHERE (value, id1, id2) > (2, 533, 444)
    ORDER BY value, id1, id2
    LIMIT 5
    

    这是您希望通过实现生成的那种查询。 请注意,它首先按用户选择的字段排序,然后按复合 id 的所有部分。

    问题是 JPA 不支持row value expression predicates(并且只有一些数据库支持它们)。您必须自己展开它们:

    (A, B) > (X, Y)
    

    变成:

    (A > X)
      OR ((A = X) AND (B > Y))
    

    由于您的 PK 有 4 个字段,因此生成的谓词会很长。

    【讨论】:

    • 谢谢@Lesiak - 我不确定我是否理解这一点:(A &gt; X) OR ((A = X) AND (B &gt; Y)) - 我不应该总是检查B = Y(例如,如果 B 是主键的一部分)?还是我误会了?
    • 我认为我们不在同一个页面上。我给了你一个展开元组比较的公式。现在,想想哪些部分构成了您的谓词:(X,Y) 是您获取的最后一条记录中的值。 (A,B) 是 JPA Paths。 (Root.get() 的结果)。我的建议是先尝试使用 SQL(Postgres 支持行值表达式谓词,因此无需展开),然后在 SQL 中展开,然后才将其转换为 JPA
    • 再次感谢@Lesiak,我已经更新了我的问题,我认为查询可能看起来像这样,如果你想看看,谢谢
    • 看起来你正朝着正确的方向前进。 tenantId、tagFieldName 和 tagId 在您的查询中是固定的,因此元组中只剩下 2 个元素。现在,让我们根据上面的公式来转换元组比较,因为元组比较在 JPA 中是无法表达的。
    • 谢谢@Lesiak,我现在已经用这些更新了我的问题,以及我遇到的其他一些问题/疑虑
    猜你喜欢
    • 1970-01-01
    • 1970-01-01
    • 2021-04-09
    • 2018-11-30
    • 2019-04-21
    • 2023-03-12
    • 2013-08-14
    • 1970-01-01
    • 1970-01-01
    相关资源
    最近更新 更多