有没有办法以高性能的方式使用嵌套分页查询实现基于 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
才能获取"仅来自此特定用户的书籍"。