分页
使用一致的字段分页模型遍历对象列表
GraphQL中的一个常见用例是遍历对象集合之间的关系。在GraphQL中暴露这些关系的方式不同,为客户端开发者提供了不同的能力集。在本页中,我们将探讨如何使用基于游标的连接模型对字段进行分页。
复数
暴露对象之间连接的最简单方法是使用返回复数列表类型的字段。例如,如果我们想获取R2-D2的朋友列表,我们可以直接要求获取所有朋友
切片
但很快,我们意识到客户端可能需要额外的行为。客户端可能希望能够指定他们想要获取多少朋友——也许他们只想要前两个。所以我们希望暴露类似这样的东西
query {
hero {
name
friends(first: 2) {
name
}
}
}但是,如果我们只获取了前两个,我们也可能想对列表进行分页;一旦客户端获取了前两个朋友,他们可能想发送第二个请求来获取接下来的两个朋友。我们如何实现这种行为呢?
分页和边(edges)
我们可以通过几种方式实现分页
- 我们可以做类似
friends(first:2 offset:2)的操作,来请求列表中的接下来的两个。 - 我们可以做类似
friends(first:2 after:$friendId)的操作,来请求获取完上一个朋友之后的接下来的两个。 - 我们可以做类似
friends(first:2 after:$friendCursor)的操作,其中我们从最后一个项目获取一个游标(cursor)并用它来进行分页。
第一个项目符号中描述的方法是经典的基于偏移量(offset-based)的分页。然而,这种分页方式可能会带来性能和安全方面的缺点,尤其是在更大的数据集上。此外,如果用户在请求一组结果后向数据库中添加了新记录,则后续页面的偏移量计算可能会变得不明确。
总的来说,我们发现基于游标(cursor-based)的分页是最强大的。特别是当游标是不透明(opaque)的时候,无论基于偏移量还是基于ID的分页都可以使用基于游标的分页来实现(通过使游标成为偏移量或ID),并且如果将来分页模型发生变化,使用游标可以提供额外的灵活性。为了提醒人们游标是不透明的,不应依赖其格式,我们建议对它们进行Base64编码。
但这给我们带来了一个问题——我们如何从对象中获取游标?我们不希望游标存在于 User 类型上;它是连接的一个属性,而不是对象的一个属性。所以我们可能需要引入一个新的间接层;我们的 friends 字段应该给我们一个边(edges)列表,而一个边同时包含一个游标和底层的节点(node)。
query {
hero {
name
friends(first: 2) {
edges {
node {
name
}
cursor
}
}
}
}“边”的概念在存在特定于边而非特定于对象的信息时也很有用。例如,如果我们想在API中暴露“友谊时间”,将其放在边上是一个自然的位置。
列表末尾、计数和连接
现在我们可以使用游标对连接进行分页了,但是我们如何知道何时到达连接的末尾呢?我们必须持续查询直到收到一个空列表,但我们希望连接能告诉我们何时到达末尾,这样就不需要额外的请求了。同样,如果我们想获取关于连接本身的额外信息,例如 R2-D2 总共有多少朋友?
为了解决这两个问题,我们的 friends 字段可以返回一个连接对象(connection object)。连接对象将是一个对象类型,它有一个用于边的字段,以及其他信息(如总计数和关于是否存在下一页的信息)。所以我们的最终查询可能看起来更像这样
query {
hero {
name
friends(first: 2) {
totalCount
edges {
node {
name
}
cursor
}
pageInfo {
endCursor
hasNextPage
}
}
}
}请注意,我们还可能在此 PageInfo 对象中包含 endCursor 和 startCursor。这样,如果我们不需要边包含的任何额外信息,我们甚至不需要查询边,因为我们已经从 pageInfo 中获得了分页所需的游标。这带来了连接可用性上的潜在改进;我们可以不只暴露 edges 列表,还暴露一个专门的仅包含节点的列表,以避免额外的间接层。
完整的连接模型
显然,这比我们最初只拥有一个复数的设计要复杂!但通过采用这种设计,我们为客户端解锁了多项功能
- 能够对列表进行分页。
- 能够查询有关连接本身的信息,例如
totalCount或pageInfo。 - 能够查询有关边本身的信息,例如
cursor或friendshipTime。 - 能够更改我们后端的分页方式,因为用户只使用不透明的游标。
为了实际看到这一点,示例 schema 中有一个额外的字段,称为 friendsConnection,它揭示了所有这些概念
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 字段,当对客户端合适时,这允许您在没有额外边间接层的情况下直接获取朋友列表。
连接规范
为了确保此模式的一致实现,Relay 项目有一个正式的规范供您遵循,用于构建使用基于游标的连接模式的 GraphQL API——无论您是否使用 Relay。
总结
总结一下对 GraphQL schema 中字段进行分页的建议:
- 可能返回大量数据的列表字段应该进行分页
- 基于游标的分页为 GraphQL schema 中的字段提供了一个稳定的分页模型
- Relay 项目中的游标连接规范为对 GraphQL schema 中的字段进行分页提供了一个一致的模式
Schema 设计
了解如何设计清晰、可适应的 schema——包括版本控制和对可空性的深思熟虑的使用。