はじめに
こんにちは!yDogです。GraphQLについて学習する過程で私が躓いた、GraphQLに登場するEdgeやNodeといった謎のフィールドに関して解説していきます。
また、ここでは架空のユーザAPIを使って説明して行こうと思います。string型
のemail
とname
を持つuser型
の配列、users
フィールドを使って説明して行きます。
それでは、まずはクエリの例を見ていただきましょう。
例
まず、Edgeが実装されているクエリとされていないクエリを比較してみましょう。
query {
users(offset: 3, limit: 10) {
name
email
}
}
query {
users(first: 10, after: "Y3Vyc29yMTIzNDU2") {
edges {
node {
name
email
}
}
}
}
一見すると複雑で冗長な表現に見えますが、この有無で機能面に大きな差が生まれます。では、これらが何のために存在しているのかについて見ていきましょう。
ページネーション
結論から言うと、EdgeやNodeはページネーションのために存在しています。ページネーションは、Googleの検索結果や、ブログの記事一覧のようなたくさんのデータを分割するための機能の一つです。
これを実装するためのデザインパターンの一つに、Cursor-based Paginationというものがあります。このパターンをGraphQL向けに洗練した、Relayの提唱する仕様がRelay-Style Cursor Paginationというものです。そして、ここにEdgeが登場します。
まずは、先ほどのEdgeがないパターンについて振り返ってみます。
Offset-based Pagination
query {
users(offset: 3, limit: 10) {
name
email
}
}
特徴
このパターンはOffset-based Paginationと呼ばれるもので、見た目から分かるとおり直感的でわかりやすい実装となっています。シンプルなユースケースや小規模なデータに適しており、スケーラビリティが求められる場合には向いていません。
メリット
-
実装が簡単
- データベースを使用している場合、SQLのクエリに
LIMIT
とOFFSET
を追加するだけで実装できる。
- データベースを使用している場合、SQLのクエリに
-
番号付きページネーションができる
- 1ページ当たりの表示数を決めておけば、offsetごとでページ管理できる。
デメリット
-
パフォーマンス
- 先述のSQLで
OFFSET
した数だけデータを読み込むため、実行速度が低下する。
- 先述のSQLで
-
データの重複
- データが頻繁に変更・削除される場合、offsetがズレて同じデータが異なるページに重複して表示されることがある。
まとめ
以上のように、Offset-based Paginationはシンプルで直感的な実装が可能な代わりに、大規模なデータセットや頻繁なデータ変更がある場合、パフォーマンスやデータの一貫性に問題が生じることがあります。
次に、こうした課題を解決するために設計された、Relay-Style Cursor Paginationについて見ていきましょう。この方法は、スケーラビリティが高く、効率的なページネーションを実現することができます。
Relay-Style Cursor Pagination
まず、はじめにで例示したEdgeあり
のコードは一部のフィールドが省略されています。すべて表示した場合はこのようになります。
query {
users(first: 10, after: "Y3Vyc29yMTIzNDU2") {
edges {
node {
name
email
}
cursor
}
pageInfo {
endCursor
hasNextPage
hasPreviousPage
startCursor
}
totalCount
}
}
特徴
このパターンはRelay-Style Cursor Paginationと呼ばれ、先述のとおりスケーラビリティが高いデザインパターンとなっています。まずは用語の説明をしてから、実際の運用方法について解説していきましょう。
用語
- Connection
- Edges
- Node
- Cursor
- Page info
Connection
Connectionは、usersConnection
のように、実体となるオブジェクトの後にConnectionと付けた命名がされ、型はUsersConnection型
となります。しかし、例ではusers
のようにConnectionが省略されています。GitHub APIやRelayのサンプルでも、このように省略された命名がされています。(なお、型名は省略されません)
サンプルコード
query {
repository(owner:"octocat", name:"Hello-World") {
issues(last:20, states:CLOSED) {
edges {
node {
title
url
labels(first:5) {
edges {
node {
name
}
}
}
}
}
}
}
}
{
user {
id
name
friends(first: 10, after: "opaqueCursor") {
edges {
cursor
node {
id
name
}
}
pageInfo {
hasNextPage
}
}
}
}
フィールドに、edges
, pageInfo
, totalCount
を持ちます。totalCount
はその名の通り、user
の全データ数を返すフィールドです。
type UserConnection {
edges: [UserEdge]
pageInfo: PageInfo
totalCount: Int
}
Edges
Edgesは、Connection型
のフィールドの一つで、Edge型
をフィールドにもつ配列
です。このクエリの場合は、userEdge型
になります。
type UserEdge {
node: User
cursor: String
}
型を見てわかる通り、node
フィールドはUser型
です。つまりは、Nodeというのは実際のデータオブジェクトそのものを指します。
Node
GraphQLには、Global Object Identificationと呼ばれる、各オブジェクトを一意に識別できるid
フィールドが付与されています。そして、Node
はid
フィールドを持つインターフェースなため、GraphQLで扱うすべてのオブジェクトがNode
インターフェースに当てはまります。
interface Node {
id: ID!
}
type User implements Node {
id: ID!
name: String!
email: String!
}
また、Node
をクエリに使うことで、オブジェクトを取得することも出来ます。この際、インターフェースから型を絞り込むために、フラグメントを使用します。
{
node(id: "MTIzNDU2") {
id
... on User {
name
email
}
}
Cursor
cursor
は、String型
で、各データの位置を示します。データの位置が分かることで、ページネーションの制御を効率的にできます。
{
users(first: 10, after: "Y3Vyc29yMTIzNDU2")
}
例えば、上記のafter
の引数はcursor
となっています。
この場合の返り値は、引数のcursor
の次のデータからになります。
これにより、Offset-based PaginationのようにOFFSET
で指定した数だけクエリを投げる必要がなくなり、パフォーマンスへの影響が大きく減ります。
なお、一般にCursorの値はIDや作成日時などのデータを元にエンコードされた文字列なため、サーバで復号してからSQLクエリが生成されます。
ここまでで、タイトルのGraphQLのEdgeやNodeってなに? の回答が出ました。
Edgeはnode
とcursor
を持つフィールド、Nodeはデータそのもの、ついでにCursorはデータの位置を表していることが分かりました。
それでは最後に、ページネーションをさらに効率的にしてくれる、Page infoについて見ていきましょう。
Page info
Page infoは、ページネーションを円滑にするためのメタデータを含んだフィールドです。
type PageInfo {
hasNextPage: Boolean!
hasPreviousPage: Boolean!
startCursor: String
endCursor: String
}
has~~
は名前の通り、このクエリでのデータの前後にデータがあるかを示し、~~Cursor
は、現在のクエリの最初と最後のデータのCursorが記述されています。
まとめ
Relay-Style Cursor Paginationは、Cursorを用いた効率的でスケーラブルな方式で、従来のOffset-based Paginationに比べ、サーバーの負荷を抑え、効率的なデータ管理を実現します。
感想
ここまで読んできた皆様ならお分かりだと思いますが、学習当初の私は、実際に重要なのはEdgeやNodeではなく、Cursorなのだと気付きませんでした。その結果、Relay-Style Cursor Paginationという単語にたどり着くのが遅くなり、無駄な時間を過ごしてしまうこととなりました。
特に、こちらの記事のおかげでGraphQLへの理解が一気に深まりました。この場をお借りしてH.Sakiさんへ感謝の言葉を伝えたいと思います。本当にありがとうございました。
参考リンク