2
2

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

GraphQLのEdgeやNodeってなに?

Posted at

はじめに

こんにちは!yDogです。GraphQLについて学習する過程で私が躓いた、GraphQLに登場するEdgeNodeといった謎のフィールドに関して解説していきます。

また、ここでは架空のユーザAPIを使って説明して行こうと思います。string型emailnameを持つuser型の配列、usersフィールドを使って説明して行きます。
それでは、まずはクエリの例を見ていただきましょう。

まず、Edgeが実装されているクエリとされていないクエリを比較してみましょう。

Edgeなし
query {
  users(offset: 3, limit: 10) {
    name
    email
  }
}
Edgeあり
query {
  users(first: 10, after: "Y3Vyc29yMTIzNDU2") {
    edges {
      node {
        name
        email
      }
    }
  }
}

一見すると複雑で冗長な表現に見えますが、この有無で機能面に大きな差が生まれます。では、これらが何のために存在しているのかについて見ていきましょう。

ページネーション

結論から言うと、EdgeやNodeはページネーションのために存在しています。ページネーションは、Googleの検索結果や、ブログの記事一覧のようなたくさんのデータを分割するための機能の一つです。

Googleのページネーション.png

これを実装するためのデザインパターンの一つに、Cursor-based Paginationというものがあります。このパターンをGraphQL向けに洗練した、Relayの提唱する仕様Relay-Style Cursor Paginationというものです。そして、ここにEdgeが登場します。

まずは、先ほどのEdgeがないパターンについて振り返ってみます。

Offset-based Pagination

Edgeなし
query {
  users(offset: 3, limit: 10) {
    name
    email
  }
}

特徴

このパターンはOffset-based Paginationと呼ばれるもので、見た目から分かるとおり直感的でわかりやすい実装となっています。シンプルなユースケースや小規模なデータに適しており、スケーラビリティが求められる場合には向いていません。

メリット

  1. 実装が簡単
    • データベースを使用している場合、SQLのクエリにLIMITOFFSETを追加するだけで実装できる。
  2. 番号付きページネーションができる
    • 1ページ当たりの表示数を決めておけば、offsetごとでページ管理できる。

デメリット

  1. パフォーマンス
    • 先述のSQLでOFFSETした数だけデータを読み込むため、実行速度が低下する。
  2. データの重複
    • データが頻繁に変更・削除される場合、offsetがズレて同じデータが異なるページに重複して表示されることがある。

まとめ

以上のように、Offset-based Paginationはシンプルで直感的な実装が可能な代わりに、大規模なデータセットや頻繁なデータ変更がある場合、パフォーマンスやデータの一貫性に問題が生じることがあります。

次に、こうした課題を解決するために設計された、Relay-Style Cursor Paginationについて見ていきましょう。この方法は、スケーラビリティが高く、効率的なページネーションを実現することができます。

Relay-Style Cursor Pagination

まず、はじめにで例示したEdgeありのコードは一部のフィールドが省略されています。すべて表示した場合はこのようになります。

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 APIRelayのサンプルでも、このように省略された命名がされています。(なお、型名は省略されません)

サンプルコード
GitHub
query {
  repository(owner:"octocat", name:"Hello-World") {
    issues(last:20, states:CLOSED) {
      edges {
        node {
          title
          url
          labels(first:5) {
            edges {
              node {
                name
              }
            }
          }
        }
      }
    }
  }
}
Relay
{
  user {
    id
    name
    friends(first: 10, after: "opaqueCursor") {
      edges {
        cursor
        node {
          id
          name
        }
      }
      pageInfo {
        hasNextPage
      }
    }
  }
}

フィールドに、edges, pageInfo, totalCountを持ちます。totalCountはその名の通り、userの全データ数を返すフィールドです。

Connection
type UserConnection {
  edges: [UserEdge]
  pageInfo: PageInfo
  totalCount: Int
}

Edges

Edgesは、Connection型のフィールドの一つで、Edge型をフィールドにもつ配列です。このクエリの場合は、userEdge型になります。

Edges
type UserEdge {
  node: User
  cursor: String
}

型を見てわかる通り、nodeフィールドはUser型です。つまりは、Nodeというのは実際のデータオブジェクトそのものを指します。

Node

GraphQLには、Global Object Identificationと呼ばれる、各オブジェクトを一意に識別できるidフィールドが付与されています。そして、Nodeidフィールドを持つインターフェースなため、GraphQLで扱うすべてのオブジェクトがNodeインターフェースに当てはまります。

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ってなに? の回答が出ました。
Edgenodecursorを持つフィールド、Nodeはデータそのもの、ついでにCursorはデータの位置を表していることが分かりました。

それでは最後に、ページネーションをさらに効率的にしてくれる、Page infoについて見ていきましょう。

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に比べ、サーバーの負荷を抑え、効率的なデータ管理を実現します。

感想

ここまで読んできた皆様ならお分かりだと思いますが、学習当初の私は、実際に重要なのはEdgeNodeではなく、Cursorなのだと気付きませんでした。その結果、Relay-Style Cursor Paginationという単語にたどり着くのが遅くなり、無駄な時間を過ごしてしまうこととなりました。
特に、こちらの記事のおかげでGraphQLへの理解が一気に深まりました。この場をお借りしてH.Sakiさんへ感謝の言葉を伝えたいと思います。本当にありがとうございました。

参考リンク

2
2
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
2
2

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?