簡體   English   中英

如何從數據庫中獲取用於在 Graphql 中進行分頁的游標?

[英]How to get a cursor for pagination in Graphql from a database?

我在獲取真正的游標以解決 GraphQL 中的數據庫分頁結果時遇到了可怕的問題。 無論我使用哪種數據庫(SQL 例如 mysql 或 NoSQL 文檔例如 mongodb),都沒有辦法,我似乎能夠獲得游標或類似游標的對象。

可能我錯過了一些基本概念,但是在搜索了我的 b 之后...關閉我開始嚴重懷疑官方的 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

我知道,不應使用基於偏移量的分頁,而是將基於光標的導航視為一種補救措施。 而且我絕對想治愈我的應用程序中的膠印病。 但是為了做到這一點,我需要能夠從某處檢索游標。

我也明白(忘了我在哪里讀到的)主鍵也不應該用於分頁。

所以,我被困在這里。

我認為你因為提出了一個好問題而被否決了。 第一個/最后一個/之前/之后的概念在 SQL 中很難實現。

我一直在為同樣的問題頭疼。 分頁文檔沒有說明在應用自定義 ORDER 語句時如何定義游標。

而且我也沒有真正在網上找到全面的解決方案。 我發現了一些人們正在解決這個問題的帖子,但答案只是部分正確或部分完整(僅對 ID 字段進行 base64 編碼以制作光標似乎是首選答案,但這對查詢的實際內容幾乎沒有說明必須做來計算游標)。 此外,任何涉及row_number 的解決方案都非常丑陋,並且不適用於不同的 SQL 方言。 因此,讓我們嘗試不同的方法。

快速免責聲明,這將是一篇相當全面的文章,但如果您的后端使用了一個不錯的查詢構建器,您可以在技術上編寫一種方法,用於將 Relay GraphQL 要求的第一個/最后一個/之前/之后的分頁實現到ANY 上預先存在的查詢。 唯一的要求是您正在排序的所有表都有一列唯一代表記錄的默認順序(通常,如果您的主鍵是整數並且使用自動生成的 ID,您可以使用該 ID,即使在技術上按主鍵對表進行排序並不總是產生與返回無序表相同的結果)

暫時忘記 base64,只需假設 ID 是表示表默認順序的有效游標字段。

您在網上找到的使用游標的答案通常是這樣的。

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

嗯,這非常適合獲取光標后的所有條目,只要您不對查詢應用任何其他類型。 一旦您使用示例中的自定義排序,此建議就會失效。

然而,其中的核心邏輯可以重新應用於帶有排序的查詢,但解決方案需要擴展。 讓我們嘗試提出完整的算法。


c 之后的前 n 個算法(光標后的前 n 個節點)

節點或邊與 SQL 術語中的行相同。 (如果 1 行代表單個實體,例如 1 個作者)

雖然游標是我們將開始返回兄弟行的行,無論是向前還是向后。

給定C是光標

A是與C進行比較的任何其他行。

TAC都是行的表。

vwxyz是表T上的 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 Av > Cv ,從輸出結果中刪除值 v (Av) 小於第一次排序 (Cv) 的 C 值的任何行。 但是,如果第一個訂單 v 的列對於 A 和 C 具有相同的值, Av = Cv我們需要查看第二個訂單列,看看是否仍然允許 A 顯示在查詢結果中。 如果Aw > Cw就會出現這種情況

讓我們繼續進行 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 語句中的最后一個排序列,以及要與之比較的最后一個條件,認識到這一點很重要。 在這種情況下 Ax > Cx(或 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

這是一個可怕的比較數量。 對於添加的每個訂單,計算c 之后的第一個 n所需的比較次數隨着對每一行執行的三角數而增長。 幸運的是,我們可以應用一些布爾代數來壓縮和優化這個查詢。

 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 個條件行我們比較下一個訂單列。

這種比較的性能也出人意料。 在第一次 Av > Cv 檢查后,所有行中平均有一半將符合條件並停止。 在通過的另一半中,大多數將在第二次 Av = Cv 檢查時失敗並停止。 因此,雖然它可能會產生大量查詢,但我不會太擔心性能。

但是,讓我們具體一點,並使用它來回答有關如何為所討論的示例使用游標的答案:

 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 個作者),但這是一個獨立於整個 first/after 的問題很重要,您還將使用 OFFSET。

處理已經具有預先存在的 WHERE 條件的查詢也不是特別困難。 您只需獲取所有預先存在的條件,將它們括在括號中,然后將它們與 AND 語句組合到算法生成的條件中。

在那里,我們實現了一種算法,可以處理任何輸入查詢並使用 first/after 對其進行正確分頁。 (如果有我錯過的邊緣情況,請告訴我)

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

如果您想符合 GraphQL Relay 規范並完全擺脫偏移,您仍然需要處理第一個 n最后一個 n前 c后 c最后 n 前 c最后 n 后 c前 n 前 c :) .

您可以使用我剛剛提供的給定 AFTER 算法進行中途。 但是對於另一半,您將需要使用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 數據庫實現第一個/最后一個/之前/之后的分頁方法。 所以這絕對是可行的。

暫無
暫無

聲明:本站的技術帖子網頁,遵循CC BY-SA 4.0協議,如果您需要轉載,請注明本站網址或者原文地址。任何問題請咨詢:yoyou2525@163.com.

 
粵ICP備18138465號  © 2020-2024 STACKOOM.COM