postgreGraphql 嵌套基于游标的分页、解析器和 SQL 查询



有没有办法以高性能的方式使用嵌套分页查询实现基于 graphql 游标的分页?

假设我们有 3 种伪 graphql 类型:

type User {
id: ID!
books: [Book!]!
}
type Book {
isbn: ID!
pages: [Page!]!
}
type Page {
num: ID!
}

为简单起见,假设用户可以阅读数千本书,每本书可以有数百页。

user数据库表:

id
1
2
3
...etc

book数据库表:

isbn
1
2
3
...etc

page数据库表:

num
1
2
3
...etc

user_book数据库表:

user_id | book_isbn
1       | 2
1       | 3
2       | 1
...etc

book_page数据库表:

book_isbn | page_num
1         | 1
1         | 2
2         | 3
...etc

我们无法加载数百万用户,他们阅读的数千本书和数百页,因此我们进行分页。假设我们要加载 100 个用户,每个用户阅读的前 50 本书和每本书的前 10 页:

{
users(first: 100, after: "9") {
edges { 
node {
id
books(first: 50) {
edges {
node {
id
pages(first: 10) {
node {
id
}
}
}
}
}
}
}
}

我们可以加载用户10..110,然后对于每个用户books解析器使用父用户id加载 50 本书,并为每本书加载 10 页pages解析器

// pseudo code
const resolvers = {
// get users from 10 to 110
users: (_, args) => `SELECT * FROM user WHERE id > args.after LIMIT 100`,
User: {
books: (root) => `SELECT * FROM book JOIN user_book ub WHERE ub.user_id = root.id LIMIT 50`
},
Book: {
pages: (root) => `SELECT * FROM page JOIN book_page bp WHERE bp.book_isbn = root.isbn LIMIT 10`
}
};

问题 1 SQL

我们执行 1 个数据库请求来获取 100 个用户,然后执行 100 个请求来获取每个用户的书籍,最后执行 500 个请求来获取每本书的页面(100 个用户 * 50 本书)。一个查询:(共有601个数据库请求

如果我们没有分页,我们可以使用数据加载器将用户 ID 批处理到书籍解析器中的数组中,将书籍 ID 批处理到页面解析器中的数组中,以仅执行 3 个数据库请求。但是,拥有用户 ID 数组10...100我们怎么能为每个用户查询 50 本书,而页面也是如此

问题 2 Graphql

我们不能将光标用于嵌套分页查询(书籍和页面),因为我们不知道它们,即使我们知道,我们也无法为每个组单独传递它们:

{
users(first: 100, after: "9") {
edges { 
node {
id
// this will not work because for each user books group we need it's own cursor
books(first: 50, after: "99") {
edges {
node {
id
}
}
}
}
}
}

我解决这个问题的唯一想法是仅允许分页作为顶级查询,而永远不要将其用作类型字段。

第一个问题主要取决于您的数据库及其可用查询。你基本上想要像"为每个用户选择前n本书">这样的东西。这可以在 SQL 中完成。请参阅:在 GROUP BY 中使用 LIMIT 来获得每组的 N 个结果?

在我看来,第二个问题有点做作:在初始页面加载时,您获取并显示 100 个用户,每个用户 50 本书,每本书 10 页。

现在考虑一下 UI:每个分散的用户都会有一个按钮,为该用户获取接下来的 50 本书。虽然不需要嵌套分页。只需为此使用books查询终结点即可。

我没有看到一个常见的现实世界场景,你想"为每个显示的用户获取接下来的 50 本书"。

graphql 模式看起来像这样(我将省略pages,因为它本质上与books相同):

Query {
users(first: Int, after: UserCursor): UserConnection
books(first: Int, after: BookCursor): BookConnection
}
type UserConnection {
edges: [UserEdge]
}
type UserEdge {
cursor: UserCursor
node: User
}
type User {
id: ID
books(first: Int): BookConnection # no `after` argument here 
}
type BookConnection {
edges: [BookEdge]
}
type BookEdge {
cursor: BookCursor # here comes the interesting part
node: Book
}
type Book {
id: ID
}

具有挑战性的部分是:">当您将此光标传递给Query.books时,BookEdge.cursor必须看起来像什么才能使嵌套分页工作?

简单的答案:游标还必须包含User.id,因为您的服务器代码必须能够为光标后面的给定用户 id 选择接下来的 50 本书,例如(简化)SELECT * FROM book WHERE user = 42 LIMIT 50 OFFSET 50

如您所见,此 GraphQL 模式没有任何嵌套分页。它只有嵌套的游标。这些游标不在乎它们是否来自架构的顶层(如Query.users)或来自某个嵌套级别(如Query.users.edges.node.books)。

唯一重要的部分是,游标必须包含服务器代码获取正确数据所需的所有信息。在这种情况下,它可能类似于cursor={Book.id, User.id}.需要Book.id才能获取"此 id之后的 50 本书">,并且需要User.id才能获取"仅来自此特定用户的书籍"。

最新更新