8
Help us understand the problem. What are the problem?
Organization

GraphQLのnodeとegdeを正しく理解してデータベースと適切に紐づける

GraphQLにはページネーションを表現する方法としてCursor Connectionsという仕様があります。

この機能を使うことでカーソルベースのページネーションを簡単に実装することができます。
仕様の詳細はGraphQLやRelayのページをご覧ください。
https://graphql.org/learn/pagination/
https://relay.dev/graphql/connections.htm

この機能はとても強力な機能なのですが、私はGraphQLを使い始めたころedgeやnodeの役割を正しく理解できておらず誤った使い方をしていました。
改めて公式ドキュメントを読んだり、その他の様々な情報に触れることで正しいと思われる使い方に気づいたので記事として残しておこうと思います。

※サンプルコードはgraphql-rubyを使ったRubyのコードで記載していますが内容はRubyに依存しないものになっていると思います。

どう間違えたのか

まずはどのように間違っていたのか紹介します。
端的にいうとGraphQLのtypeとデータベースのテーブルを1体1で紐づけて作成していました。
具体例があった方がわかりやすいので下記の例を使って説明します。

モデル

下記の2つのモデルがあるとします。

  • ユーザー
    • 複数の組織に所属できる
  • 組織
    • 複数のユーザーが所属している

ER図

ユーザーと組織は多対多なので中間テーブル(user_organizations)を使って表現しています。

graphql-er..png

最初の実装

例えばユーザー情報を返却するクエリーを定義するとします。
type Queryに定義しているuser(userId: Int!): Userがそれです。

当時の私は"type"="テーブル"で実装していたので、UserとUserOrganizationとOrganizationのtypeを作成していました。
connection関連のtypeも加えると下記のようになります。

type Query {
  user(userId: Int!): User
}

type User {
  id: ID!
  name: String!
  userOrganizations(
    after: String
    before: String
    first: Int
    last: Int
  ): UserOrganizationConnection!
}

type UserOrganizationConnection {
  edges: [UserOrganizationEdge]
  nodes: [UserOrganization!]!
  pageInfo: PageInfo!
}

type UserOrganizationEdge {
  cursor: String!
  node: UserOrganization
}

type UserOrganization {
  id: ID!
  organization: Organization!
  user: User!
}

type Organization {
  id: ID!
  name: String!
  userOrganizations(
    after: String
    before: String
    first: Int
    last: Int
  ): UserOrganizationConnection!
}

このクエリーを実行すると下記のようになります。
UserOrganizationsConnectionのnodeはUserOrganization typeとなっており、その中のフィールドの1つとしてOrganizationが入っています。

query User($userId: Int!) {
  user(userId: $userId) {
    id
    name
    userOrganizations {
      edges {
        node {
          organization {
            name
          }
        }
      }
    }
  }
}

userId=1のユーザーがorg1, org2, org3の3つの組織に所属している場合、下記のようなレスポンスになります。

{
  "data": {
    "user": {
      "id": "1",
      "name": "ham",
      "userOrganizations": {
        "edges": [
          {
            "node": {
              "organization": {
                "name": "org1"
              }
            }
          },
          {
            "node": {
              "organization": {
                "name": "org2"
              }
            }
          },
          {
            "node": {
              "organization": {
                "name": "org3"
              }
            }
          }
        ]
      }
    }
  }
}

これでも問題なく動作はするのですが、connectionで使われているnodeやedgeの意味を正しく理解してから改めて考えるとこの使い方は間違いだったと気づきました。

node, edgeとは

Connectionの用語はグラフ理論から来ています。
グラフ理論についての詳細はこの記事では省きますがnodeはグラフ『頂点』、edgeは頂点と頂点を繋ぐ『辺』を意味しています。
下記がグラフのイメージです。
graph.png

先程の例をグラフで表現すると下記のようになります。
OrganizationがUserOrganizationの1つのfieldとして扱われていて歪な状態になっています。

graph1.png

グラフの意味を踏まえて考えるとUserOrgnizationはUserとOrganizationの紐付け情報なのでedgeとして表現するのがふさわしいということがわかります。
図で表すと下記の通り。先程のグラフよりしっくりくると思います。

graph2.png

これを踏まえて実装をやり直してみます。

やり直した実装

UserExaminationは明示的には定義せず、OrganizationEdgeとUserConnectionがUserとOrganizationを紐づける役割を持っています。

type Query {
  user(userId: Int!): User
}

type User {
  email: String!
  id: ID!
  name: String!
  organizations(
    after: String
    before: String
    first: Int
    last: Int
  ): OrganizationConnection!
}

type OrganizationConnection {
  edges: [OrganizationEdge]
  nodes: [Organization!]!
  pageInfo: PageInfo!
}

type OrganizationEdge {
  cursor: String!
  node: Organization
}

type Organization {
  id: ID!
  name: String!
  users(
    after: String
    before: String
    first: Int
    last: Int
  ): UserConnection!
}

type UserConnection {
  edges: [UserEdge]
  nodes: [User!]!
  pageInfo: PageInfo!
}

実行するクエリーは下記の通り。
OrganizationConnectionのnodeがOrganization typeになったため先ほどよりシンプルに書くことができます。

query User($userId: Int!) {
  user(userId: $userId) {
    id
    name
    organizations {
      edges {
        node {
          name
        }
      }
    }
  }
}

先ほどと同じデータの場合、下記のレスポンスになります。

{
  "data": {
    "user": {
      "id": "1",
      "name": "ham",
      "organizations": {
        "edges": [
          {
            "node": {
              "name": "org1"
            }
          },
          {
            "node": {
              "name": "org2"
            }
          },
          {
            "node": {
              "name": "org3"
            }
          }
        ]
      }
    }
  }
}

今回の場合、最初の実装でも問題なく動作はしていましたが、正しく意図を理解した実装にすることでシンプルなクエリーにすることができました。
Railsの場合、モデル(テーブル)を中心に考えがちで、油断したら最初に実装したようにtype=モデル(テーブル)で実装してしまいそうになるので気をつけないと思いました。

Why not register and get more from Qiita?
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away
8
Help us understand the problem. What are the problem?