简体   繁体   English

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

[英]Graphql nested cursor based pagination, resolver and SQL query

Is there a way to implement graphql cursor based pagination with nested pagination queries in a performant way?有没有办法以高性能的方式使用嵌套分页查询实现基于 graphql游标的分页

Let's say we have 3 pseudo graphql types:假设我们有 3 个伪 graphql 类型:

type User {
  id: ID!
  books: [Book!]!
}

type Book {
  isbn: ID!
  pages: [Page!]!
}

type Page {
  num: ID!
}

For simplicity let's say user could read thousands of books and each book can have hundred of pages.为简单起见,假设用户可以阅读数千本书,每本书可以有数百页。

user database table: user数据库表:

id
1
2
3
...etc

book database table: book数据库表:

isbn
1
2
3
...etc

page database table: page数据库表:

num
1
2
3
...etc

user_book database table: user_book数据库表:

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

book_page database table: book_page数据库表:

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

We can't load million users, thousands of books they read and hundreds of pages, so we do a pagination.我们无法加载百万用户、他们阅读的数千本书和数百页,所以我们进行了分页。 Let's say we want to load 100 users, first 50 books that each of them read and first 10 pages of each book:假设我们要加载 100 个用户,他们每个人阅读的前 50 本书以及每本书的前 10 页:

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

We can load users 10..110 , then for each user books resolver use parent user id to load 50 books and for each book pages resolver load 10 pages:我们可以加载用户10..110 ,然后为每个用户books解析器使用父用户id加载 50 本书,并为每个图书pages解析器加载 10 页:

// 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`
  }
};

Problem 1 SQL问题1 SQL

We do 1 database request to get 100 users, then 100 requests to get books for each user and finally 500 requests to get pages for each book (100 users * 50 books).我们执行 1 个数据库请求以获取 100 个用户,然后执行 100 个请求以获取每个用户的书籍,最后执行 500 个请求以获取每本书的页面(100 个用户 * 50 本书)。 Total of 601 database request for one query :(一个查询总共有601个数据库请求 :(

If we didn't have pagination we could use dataloader to batch user ids into array in books resolver and book ids in pages resolver to do only 3 database requests.如果我们没有分页,我们可以使用 dataloader 将用户 ID 批处理到书籍解析器中的数组和页面解析器中的书籍 ID 以仅执行 3 个数据库请求。 But how can having array of user ids 10...100 can we query only 50 books for each user and the same for pages?但是,如何拥有10...100用户 ID 的数组,我们如何才能为每个用户只查询 50 本书而对页面也一样呢?

Problem 2 Graphql问题 2 Graphql

We can't use cursor for nested paginated queries (books and pages) because we don't know them and even if we did we could not pass them separately for each group:我们不能将游标用于嵌套的分页查询(书籍和页面),因为我们不知道它们,即使我们知道,我们也不能为每个组分别传递它们:

{
  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
           }
         }
       }
     }
   }
}

My only idea to solve this is to allow pagination only as top level query and never use it as field on type.我解决这个问题的唯一想法是只允许分页作为顶级查询,并且永远不要将其用作类型字段。

Your first problem mostly depends on your database and its available queries.您的第一个问题主要取决于您的数据库及其可用查询。 You basically want something like "select the top n books for each user" .您基本上想要“为每个用户选择前 n 本书”之类的东西。 This can be done in SQL.这可以在 SQL 中完成。 See: Using LIMIT within GROUP BY to get N results per group?请参阅: 在 GROUP BY 中使用 LIMIT 以获得每组 N 个结果?

The second problem feels a bit contrived in my opinion: On your initial page load, you fetch and display 100 users, and for each user 50 books, and for each book 10 pages.第二个问题在我看来有点做作:在初始页面加载时,您获取并显示 100 个用户,并且为每个用户显示 50 本书,并且为每本书显示 10 页。

Now think in terms of the UI: There will be a button for each displyed user to fetch the next 50 books for that user.现在考虑一下 UI:每个显示的用户都会有一个按钮,可以为该用户获取接下来的 50 本书。 Though there's no need for nested pagination.虽然不需要嵌套分页。 You can simply use the books query endpoint for that.您可以简单地使用books查询端点。

I don't see a common real world scenario where you want to "fetch the next 50 books for every displayed user" .我没有看到一个常见的现实世界场景,您希望“为每个显示的用户获取接下来的 50 本书”

The graphql schema would look something like this (I'll omit pages , because it is essentially the same as books ): 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
}

The challenging part is: "What must BookEdge.cursor look like to make nested pagination work when you pass this cursor to Query.books ?"具有挑战性的部分是: “当您将此光标传递给Query.books时, BookEdge.cursor必须是什么样子才能使嵌套分页工作?”

Simple answer: The cursor must additionally include the User.id , because your server code must be able to select the next 50 books for the given user id after the cursor , eg (simplified) SELECT * FROM book WHERE user = 42 LIMIT 50 OFFSET 50 .简单的回答:光标必须另外包含User.id ,因为您的服务器代码必须能够在光标之后为给定的用户 id选择接下来的 50 本书,例如(简化的) SELECT * FROM book WHERE user = 42 LIMIT 50 OFFSET 50 .

As you can see, this GraphQL schema does not have any nested pagination.如您所见,此 GraphQL 模式没有任何嵌套分页。 It just has nested cursors.它只有嵌套的游标。 And those cursors don't care if they originate from the top level of the schema (like Query.users ) or from some nested level (like Query.users.edges.node.books ).这些游标并不关心它们是来自架构的顶层(如Query.users )还是来自某个嵌套级别(如Query.users.edges.node.books )。

The only important part is, that the cursor must include all necessary information for your server code to fetch the correct data.唯一重要的部分是,游标必须包含服务器代码获取正确数据所需的所有信息。 In this case it might be something like cursor={Book.id, User.id} .在这种情况下,它可能类似于cursor={Book.id, User.id} The Book.id is needed to be able to fetch "the next 50 books after this id" and the User.id is needed to fetch "books from only this particular user" . Book.id需要能够获取“此 id之后的下 50 本书” ,而User.id需要获取“仅来自该特定用户的书”

声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM