学习分页

分页

使用一致的字段分页模型遍历对象列表

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 对象中包含 endCursorstartCursor。这样,如果我们不需要边包含的任何额外信息,我们甚至不需要查询边,因为我们已经从 pageInfo 中获得了分页所需的游标。这带来了连接可用性上的潜在改进;我们可以不只暴露 edges 列表,还暴露一个专门的仅包含节点的列表,以避免额外的间接层。

完整的连接模型

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

  • 能够对列表进行分页。
  • 能够查询有关连接本身的信息,例如 totalCountpageInfo
  • 能够查询有关边本身的信息,例如 cursorfriendshipTime
  • 能够更改我们后端的分页方式,因为用户只使用不透明的游标。

为了实际看到这一点,示例 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——包括版本控制和对可空性的深思熟虑的使用。

前往下一课 教程