如何从数据库中获取 Graphql 中的分页游标?

Posted

技术标签:

【中文标题】如何从数据库中获取 Graphql 中的分页游标?【英文标题】:How to get a cursor for pagination in Graphql from a database? 【发布时间】:2019-11-21 04:39:24 【问题描述】:

我在获取真正的游标以解决 GraphQL 中的数据库分页结果时遇到了可怕的问题。不管我用的是什么数据库(SQL如mysql或NoSQL文档如mongodb),都没有办法,我好像能得到一个游标或游标对象。

可能我错过了一些基本概念,但是在搜索了我的 b...off 之后,我开始严重怀疑官方的 GraphQL 分页文档是否

https://graphql.org/learn/pagination/

完全基于任何真实的现场体验。

这是我的问题:我怎样才能从这样的 SQL 查询中获得任何与游标相似的东西?

SELECT authors.id, authors.last_name, authors.created_at FROM authors
ORDER BY authors.last_name, author.created_at
LIMIT 10
OFFSET 20

我知道,不应该使用基于偏移的分页,而是将基于光标的导航视为一种补救措施。而且我肯定想治愈我的应用程序中的偏移疾病。但为了做到这一点,我需要能够从 somewhere 检索光标。

我也明白(忘记我在哪里看到的)主键也不应该用于分页。

所以,我被困在这里了。

【问题讨论】:

也许this article 会有所帮助。您可以首先获取元素的行号,然后使用它使用传统偏移量返回该行之后的行。 【参考方案1】:

我认为你因为提出了一个好问题而被否决了。 first/last/before/after 的概念很难在 SQL 中实现。

我一直在为同样的问题头疼。分页文档没有说明在应用自定义 ORDER 语句时如何定义游标。

而且我也没有真正在网上找到全面的解决方案。我发现了一些人们正在解决这个问题的帖子,但答案只是部分正确或部分完整(只是对 ID 字段进行 base64 编码以使光标看起来是首选答案,但这对查询的实际内容几乎没有说明必须做计算光标)。此外,任何涉及 row_number 的解决方案都非常丑陋,并且不适用于不同的 SQL 方言。所以让我们尝试不同的方式。

快速免责声明,这将是一篇相当全面的帖子,但如果您的后端使用了不错的查询构建器,您可以在技术上编写一个方法来实现 Relay 所需的第一个/最后/之前/之后分页GraphQL 到 ANY 预先存在的查询。唯一的要求是您要排序的所有表都有一个唯一代表记录的默认顺序的列(通常如果您的主键是一个整数并且使用自动生成的 ID,您可以使用那个,即使在技术上按主键排序表并不总是与返回无序表产生相同的结果)

暂时忘掉base64,假设ID是一个有效的游标字段,代表表的默认顺序。

您在网上找到的使用光标的答案通常是这样的。

SELECT * FROM TABLE T
WHERE T.id > $cursorId;

好吧,这对于获取光标后的所有条目非常有用,只要您不对查询应用任何其他排序即可。一旦您使用示例中的自定义排序,此建议就会失效。

但是,其中的核心逻辑可以重新应用于带有排序的查询,但需要扩展解决方案。让我们试着想出完整的算法。


c 之后的前 n 个算法 (光标后的前 n 个节点)

节点或边与 SQL 术语中的行相同。 (如果 1 行表示单个实体,例如 1 个作者)

虽然光标是我们将开始返回同级行的行,无论是向前还是向后。

鉴于 C 是光标

A 是与 C 进行比较的任何其他行。

TAC 都是行的表。

vwxyzT表上的5列,自然AC都有这些列。

算法必须根据游标对象、给定 n 和这 5 列的提供顺序来决定返回查询中是否包含或排除 A。

让我们从一个订单开始。

假设有 1 个顺序 (v):(如果我们假设我们的表默认按其主键排序,那么至少应该始终存在) 为了显示前 n 条记录,我们需要应用 限制为 n,这很简单。困难的部分是在c之后

对于仅按 1 个字段排序的表,将归结为:

 SELECT A FROM T
 WHERE A.v > C.v
 ORDER BY T.v ASC
 LIMIT n

这应该显示所有 v 大于 C 的行,并删除所有 v 小于 C 的行,这意味着在 C 之前不会留下任何行。如果我们假设主键正确表示自然订购,我们 可以删除 ORDER BY 语句。那么这个查询的可读性稍强的版本将变为:

 SELECT A FROM T
 WHERE A.id > $cursorIdGivenByClient
 LIMIT n

在那里,我们找到了为“未排序”表提供光标的最简单解决方案。这与处理游标的普遍接受的答案相同,但不完整。

现在让我们看一个按两列(vw)排序的查询:

 SELECT A FROM T
 WHERE A.v > C.v
 OR (A.v = C.v AND A.w > C.w)
 ORDER BY T.v ASC, T.w ASC
 LIMIT n

我们从相同的WHERE A.v > C.v 开始,从输出结果中删除任何值 v (A.v) 小于 C 值的第一次排序 (C.v) 的行。但是,如果一阶 v 的列对于 A 和 C 具有相同的值,A.v = C.v 我们需要查看二阶列以查看是否仍然允许在查询结果中显示 A。如果A.w > C.w

让我们继续进行 3 种排序的查询:

 SELECT A FROM T
 WHERE A.v > C.v
 OR (A.v = C.v AND A.w > C.w)
 OR (A.v = C.v AND A.w = C.w AND A.x > C.x)
 ORDER BY T.v ASC, T.w ASC, T.x ASC
 LIMIT n

这与 2 个排序的逻辑相同,但要解决的更多一些。如果第一列相同,我们需要查看第二列,看看谁是最大的。如果第二列也相同,我们需要查看第三列。重要的是要认识到主键始终是 ORDER BY 语句中的最后一个排序列,也是要比较的最后一个条件。在这种情况下 A.x > C.x(或 A.id > $cursorId)

无论如何,一个模式应该开始出现。要对 4 列进行排序,查询将如下所示:

 SELECT A FROM T
 WHERE A.v > C.v
 OR (A.v = C.v AND A.w > C.w)
 OR (A.v = C.v AND A.w = C.w AND A.x > C.x)
 OR (A.v = C.v AND A.w = C.w AND A.x = C.x AND A.y > C.y)
 ORDER BY T.v ASC, T.w ASC, T.x ASC, T.y ASC
 LIMIT n

最后对 5 列进行排序。

 SELECT A FROM T
 WHERE A.v > C.v
 OR (A.v = C.v AND A.w > C.w)
 OR (A.v = C.v AND A.w = C.w AND A.x > C.x)
 OR (A.v = C.v AND A.w = C.w AND A.x = C.x AND A.y > C.y)
 OR (A.v = C.v AND A.w = C.w AND A.x = C.x AND A.y = C.y AND A.z > C.z)
 ORDER BY T.v ASC, T.w ASC, T.x ASC, T.y ASC, T.z ASC
 LIMIT n

这是一个可怕的比较。对于添加的每个订单,计算 first n after c 所需的比较次数会增加每行执行的Triangular Number。幸运的是,我们可以应用一些布尔代数来压缩和优化这个查询。

 SELECT A FROM T
 WHERE (A.v > C.v OR
           (A.v = C.v AND 
              (A.w > C.w OR
                   (A.w = C.w AND
                       (A.x > C.x OR
                           (A.x = C.x AND
                               (A.y > C.y OR
                                    (A.y = C.y AND
                                        (A.z > C.z)))))))))
 ORDER BY T.v ASC, T.w ASC, T.x ASC, T.y ASC, T.z ASC
 LIMIT n

即使在浓缩之后,图案也很清晰。每个条件行在 OR 和 AND 之间变化,每个条件行在 > 和 = 之间变化,最后每 2 个条件行我们比较下一个顺序列。

而且这种比较的性能也令人惊讶。平均一半的行将在第一次 A.v > C.v 检查后合格并停在那里。在通过的另一半中,大多数将在第二次 A.v = C.v 检查时失败并停在那里。因此,虽然它可能会产生大量查询,但我不会太担心性能。

但是,让我们具体一点,并使用它来为您提供有关如何在相关示例中使用光标的答案:

 SELECT authors.id, authors.last_name, authors.created_at FROM authors
 ORDER BY authors.last_name, author.created_at

您的基本查询是否已排序,但尚未分页。

您的服务器收到一个请求以显示“作者后的前 20 位作者和光标” 将光标解码后,我们发现它代表的是id为15的作者。

首先,我们可以运行一个小的前置查询来获取我们需要的必要信息:

 $authorLastName, $authorCreatedAt =
      SELECT authors.last_name, authors.created_at from author where id = 15;

然后我们应用算法并替换字段:

  SELECT a.id, a.last_name, a.created_at FROM authors a
  WHERE (a.last_name > $authorLastName OR
            (a.last_name = $authorLastName AND 
               (a.created_at > $authorCreatedAt OR
                    (a.created_at = $authorCreatedAt AND
                        (a.id > 15)))))
 ORDER BY a.last_name, a.created_at, a.id
 LIMIT 20;

此查询将根据查询的种类正确返回 ID 为 15 的作者之后的前 20 位作者。

如果您不喜欢使用变量或辅助查询,您也可以使用子查询:

  SELECT a.id, a.last_name, a.created_at FROM authors a
  WHERE (a.last_name > (select last_name from authors where id 15) OR
            (a.last_name = (select last_name from authors where id 15) AND 
               (a.created_at > (select created_at from authors where id 15)  OR
                    (a.created_at = (select created_at from authors where id 15) AND
                        (a.id > 15)))))
 ORDER BY a.last_name, a.created_at, a.id
 LIMIT 20;

同样,这并不像看起来那么糟糕,子查询不相关,结果将缓存在行循环中,因此它不会对性能特别不利。但是查询确实会变得混乱,尤其是当您开始使用 JOINS 时,这也需要在子查询中应用。

您不需要在 a.id 上显式调用 ORDER,但我这样做是为了与算法保持一致。如果您使用 DESC 而不是 ASC,这确实变得非常重要。

那么如果使用 DESC 列而不是 ASC 会发生什么?算法有问题吗?如果你应用一个小的额外规则,那就不行了。对于使用 DESC 而不是 ASC 的任何列,您将 '>' 符号替换为 '

JOINS 对此算法没有影响(谢天谢地),除了联接表中的 20 行不一定代表 20 个实体(在本例中为 20 个作者),但这是一个独立于整体的问题第一/后事,你也可以使用 OFFSET。

处理已经存在 WHERE 条件的查询也不是特别困难。您只需获取所有预先存在的条件,将它们用括号括起来,然后用 AND 语句将它们组合到算法生成的条件中。

在那里,我们实现了一种算法,可以处理任何输入查询并使用 first/after 对其进行正确分页。 (如果有我遗漏的边缘情况,请告诉我)

你可以停在那里,但是......不幸的是

你仍然需要处理first nlast nbefore cafter clast n before c, last n after cfirst n before c 如果您想符合 GraphQL Relay 规范并完全摆脱偏移:)。

使用我刚刚提供的给定 AFTER-algorithm,您可以完成一半。但对于另一半,您将需要使用 BEFORE 算法。跟AFTER算法很像:

 SELECT A FROM T
 WHERE (A.v < C.v OR
           (A.v = C.v AND 
              (A.w < C.w OR
                   (A.w = C.w AND
                       (A.x < C.x OR
                           (A.x = C.x AND
                               (A.y < C.y OR
                                    (A.y = C.y AND
                                        (A.z < C.z)))))))))
 ORDER BY T.v ASC, T.w ASC, T.x ASC, T.y ASC, T.z ASC
 LIMIT n

要获得 BEFORE 算法,您可以使用 AFTER 算法,只需将所有 '' 运算符,反之亦然。 (所以本质上 before 和 after 是相同的算法,BEFORE/AFTER + ASC/DESC 决定操作员必须指向哪个方向。)

对于“first n”,您无需执行任何操作,只需将“LIMIT n”应用于查询。

对于“last n”,您需要应用“LIMIT n”并反转所有给定的 ORDERS,将 ASC 与 DESC 和 DESC 与 ASC 切换。 'last n' 有一个警告,虽然它会正确返回最后 n 条记录,但它会以相反的顺序执行此操作,因此您需要再次手动反转返回的集合,无论是在您的数据库中还是在您的代码中。

通过这些规则,您可以成功地将来自 Relay GraphQL 规范的任何分页请求集成到任何 SQL 查询中,使用唯一的可排序列(通常是主键)作为代表表默认排序的真实来源的游标。

这非常令人生畏,但我设法使用这些算法为 Doctrine DQL 构建器编写了一个插件,以使用 MySQL 数据库实现 first/last/before/after 分页方法。所以这绝对是可行的。

【讨论】:

您不会相信这有多大帮助。非常感谢您抽出宝贵时间撰写此详细答案。 这非常有帮助!如果没有 Int ID 作为主键,是否有任何最佳实践来解决这个问题?因为我们通常使用 GUID,但它们当然没有排序...... 如果您订购姓氏并且另一个用户更改了姓氏,您可能会在两个不同的页面上获得相同的记录。当结果按不同于 id 的字段排序时,有没有办法保证光标分页不重复?

以上是关于如何从数据库中获取 Graphql 中的分页游标?的主要内容,如果未能解决你的问题,请参考以下文章

使用 Mongoose 进行基于 GraphQL 游标的分页 [关闭]

在使用 graphql 缓存的基于游标的分页中跟踪页码

如何在 Relay 中管理游标和排序?

在 GraphQL 查询中分组后如何限制和跳过标签列表页面的分页?

具有多列的基于 MySQL 游标的分页

readQuery 不适用于 Apollo 和 GraphQL 应用程序中的分页