学习教程
分页

分页

¥Pagination

Traverse lists of objects with a consistent field pagination model

GraphQL 中的一个常见用例是遍历对象集之间的关系。在 GraphQL 中可以通过不同的方式公开这些关系,从而为客户端开发者提供不同的功能。在此页面上,我们将探索如何使用基于游标的连接模型对字段进行分页。

¥A common use case in GraphQL is traversing the relationship between sets of objects. There are different ways that these relationships can be exposed in GraphQL, giving a varying set of capabilities to the client developer. On this page, we’ll explore how fields may be paginated using a cursor-based connection model.

复数

¥Plurals

公开对象之间连接的最简单方法是使用返回复数 列表类型 的字段。例如,如果我们想获取 R2-D2 的朋友列表,我们可以只要求他们全部:

¥The simplest way to expose a connection between objects is with a field that returns a plural List type. For example, if we wanted to get a list of R2-D2’s friends, we could just ask for all of them:

Operation
Response

切片

¥Slicing

不过,我们很快意识到客户可能还需要其他行为。客户端可能希望能够指定他们想要获取多少个朋友 - 也许他们只想要前两个。因此,我们希望公开类似这样的内容:

¥Quickly, though, we realize that there are additional behaviors a client might want. A client might want to be able to specify how many friends they want to fetch—maybe they only want the first two. So we’d want to expose something like this:

query {
  hero {
    name
    friends(first: 2) {
      name
    }
  }
}

但如果我们只获取前两个,我们可能还想对列表进行分页;一旦客户端获取了前两个朋友,他们可能想要发送第二个请求来询问接下来的两个朋友。我们怎样才能实现这种行为呢?

¥But if we just fetched the first two, we might want to paginate through the list as well; once the client fetches the first two friends, they might want to send a second request to ask for the next two friends. How can we enable that behavior?

分页和边缘

¥Pagination and edges

我们可以通过几种方式进行分页:

¥There are several ways we could do pagination:

  • 我们可以执行类似 friends(first:2 offset:2) 的操作来请求列表中的下两个。

    ¥We could do something like friends(first:2 offset:2) to ask for the next two in the list.

  • 我们可以做类似 friends(first:2 after:$friendId) 的事情,在我们获取最后一个朋友之后询问接下来的两个。

    ¥We could do something like friends(first:2 after:$friendId), to ask for the next two after the last friend we fetched.

  • 我们可以做类似 friends(first:2 after:$friendCursor) 的事情,我们从最后一项获取光标并使用它来分页。

    ¥We could do something like friends(first:2 after:$friendCursor), where we get a cursor from the last item and use that to paginate.

第一点中描述的方法是基于偏移量的经典分页。但是,这种分页样式可能会影响性能和安全性,尤其是对于较大的数据集。此外,如果在用户请求一页结果后将新记录添加到数据库中,则后续页面的偏移量计算可能会变得不明确。

¥The approach described in the first bullet is classic offset-based pagination. However, this style of pagination can have performance and security downsides, especially for larger data sets. Additionally, if new records are added to the database after the user has made a request for a page of results, then offset calculations for subsequent pages may become ambiguous.

一般来说,我们发现基于光标的分页是设计中最强大的。特别是如果游标是不透明的,则可以使用基于游标的分页(通过使游标成为偏移量或 ID)来实现基于偏移量或基于 ID 的分页,并且如果将来分页模型发生变化,使用游标可以提供额外的灵活性。提醒一下,游标是不透明的,不应依赖其格式,我们建议使用 base64 编码。

¥In general, we’ve found that cursor-based pagination is the most powerful of those designed. Especially if the cursors are opaque, either offset or ID-based pagination can be implemented using cursor-based pagination (by making the cursor the offset or the ID), and using cursors gives additional flexibility if the pagination model changes in the future. As a reminder that the cursors are opaque and their format should not be relied upon, we suggest base64 encoding them.

但这给我们带来了一个问题 - 我们如何从对象中获取光标?我们不希望光标停留在 User 类型上;它是连接的属性,而不是对象的属性。所以我们可能想引入一个新的间接层;我们的 friends 字段应该为我们提供一个边列表,并且一条边同时具有光标和底层节点:

¥But that leads us to a problem—how do we get the cursor from the object? We wouldn’t want the cursor to live on the User type; it’s a property of the connection, not of the object. So we might want to introduce a new layer of indirection; our friends field should give us a list of edges, and an edge has both a cursor and the underlying node:

query {
  hero {
    name
    friends(first: 2) {
      edges {
        node {
          name
        }
        cursor
      }
    }
  }
}

如果存在特定于边缘而不是特定于对象之一的信息,则边缘的概念也被证明是有用的。例如,如果我们想在 API 中公开 “友谊时间”,那么将其放在边缘是一个自然的放置位置。

¥The concept of an edge also proves useful if there is information that is specific to the edge, rather than to one of the objects. For example, if we wanted to expose “friendship time” in the API, having it live on the edge is a natural place to put it.

列表末尾、计数和连接

¥End-of-list, counts, and connections

现在我们可以使用游标分页连接,但我们如何知道何时到达连接的末尾?我们必须继续查询,直到得到一个空列表,但我们希望连接告诉我们何时到达末尾,这样我们就不需要额外的请求了。类似地,如果我们想要有关连接本身的其他信息,例如,R2-D2 总共有多少个朋友,该怎么办?

¥Now we can paginate through the connection using cursors, but how do we know when we reach the end of the connection? We have to keep querying until we get an empty list back, but we’d like for the connection to tell us when we’ve reached the end so we don’t need that additional request. Similarly, what if we want additional information about the connection itself, for example, how many friends does R2-D2 have in total?

为了解决这两个问题,我们的 friends 字段可以返回一个连接对象。连接对象将是一个对象类型,它具有一个用于边缘的字段,以及其他信息(如总数和有关下一页是否存在的信息)。因此,我们的最终查询可能看起来更像这样:

¥To solve both of these problems, our friends field can return a connection object. The connection object will be an Object type that has a field for the edges, as well as other information (like total count and information about whether a next page exists). So our final query might look more like this:

query {
  hero {
    name
    friends(first: 2) {
      totalCount
      edges {
        node {
          name
        }
        cursor
      }
      pageInfo {
        endCursor
        hasNextPage
      }
    }
  }
}

请注意,我们还可能在此 PageInfo 对象中包含 endCursorstartCursor。这样,如果我们不需要边缘包含的任何附加信息,则根本不需要查询边缘,因为我们从 pageInfo 获得了分页所需的游标。这可能会提高连接的可用性;我们不仅可以公开 edges 列表,还可以公开仅包含节点的专用列表,以避免间接层。

¥Note that we also might include endCursor and startCursor in this PageInfo object. This way, if we don’t need any of the additional information that the edge contains, we don’t need to query for the edges at all, since we got the cursors needed for pagination from pageInfo. This leads to a potential usability improvement for connections; instead of just exposing the edges list, we could also expose a dedicated list of just the nodes, to avoid a layer of indirection.

完整连接模型

¥Complete connection model

显然,这比我们最初设计的只有复数形式更复杂!但通过采用这种设计,我们为客户端解锁了多项功能:

¥Clearly, this is more complex than our original design of just having a plural! But by adopting this design, we’ve unlocked several capabilities for the client:

  • 对列表进行分页的能力。

    ¥The ability to paginate through the list.

  • 能够询问有关连接本身的信息,例如 totalCountpageInfo

    ¥The ability to ask for information about the connection itself, like totalCount or pageInfo.

  • 能够询问有关边缘本身的信息,例如 cursorfriendshipTime

    ¥The ability to ask for information about the edge itself, like cursor or friendshipTime.

  • 由于用户只使用不透明的光标,因此能够更改后端的分页方式。

    ¥The ability to change how our backend does pagination, since the user just uses opaque cursors.

要查看实际操作,示例架构中还有一个名为 friendsConnection 的附加字段,它公开了所有这些概念:

¥To see this in action, there’s an additional field in the example schema, called friendsConnection, that exposes all of these concepts:

interface Character {
  id: ID!
  name: String!
  friends: [Character]
  friendsConnection(first: Int, after: ID): FriendsConnection!
  appearsIn: [Episode]!
}
 
type FriendsConnection {
  totalCount: Int
  edges: [FriendsEdge]
  friends: [Character]
  pageInfo: PageInfo!
}
 
type FriendsEdge {
  cursor: ID!
  node: Character
}
 
type PageInfo {
  startCursor: ID
  endCursor: ID
  hasNextPage: Boolean!
}

你可以在示例查询中尝试一下。尝试删除 friendsConnection 字段的 after 参数,看看分页会受到怎样的影响。此外,尝试在连接上用辅助 friends 字段替换 edges 字段,这可以让你直接进入朋友列表而无需额外的边缘层间接,当适用于客户端时:

¥You can try it out in the example query. Try removing the after argument for the friendsConnection field to see how the pagination will be affected. Also, try replacing the edges field with the helper friends field on the connection, which lets you get directly to the list of friends without the additional edge layer of indirection, when appropriate for clients:

Operation
Response

连接规范

¥Connection specification

为了确保此模式的一致实现,Relay 项目有一个正式的 specification,你可以遵循它来构建使用基于游标的连接模式的 GraphQL API - 是否使用你的中继。

¥To ensure a consistent implementation of this pattern, the Relay project has a formal specification you can follow for building GraphQL APIs that use a cursor-based connection pattern - whether or not use you Relay.

回顾

¥Recap

回顾这些关于在 GraphQL 模式中分页字段的建议:

¥To recap these recommendations for paginating fields in a GraphQL schema:

  • 可能返回大量数据的列表字段应该分页

    ¥List fields that may return a lot of data should be paginated

  • 基于游标的分页为 GraphQL 模式中的字段提供了稳定的分页模型

    ¥Cursor-based pagination provides a stable pagination model for fields in a GraphQL schema

  • Relay 项目中的游标连接规范为在 GraphQL 模式中对字段进行分页提供了一致的模式

    ¥The cursor connection specification from the Relay project provides a consistent pattern for paginating the fields in a GraphQL schema