Open13

GraphQL

ikeponikepon

GraphQLは、Facebookが開発しているWeb APIのための規格
「クエリ言語」と「スキーマ言語」からなる

  • クエリ言語
    • query: データ取得系
    • mutation: データ更新系
    • subscription: サーバーからのイベント通知
  • スキーマ言語
    • GraphQL API の仕様を記述するための言語
ikeponikepon
  • スキーマの例
type Query { # type Query は query のための予約されたルートの型名
  currentUser: User! # フィールド、 ! は null にならないという意味
}

type User {
  id: ID!
  name: String!
}
  • リゾルバ

    • 一つ一つのフィールドに Resolver と呼ばれる関数がマッピングされる
  • 上記スキーマに対するクエリの例

query GetCurrentUser { # GetCurrentUser はこのクエリ名
  currentUser { # currentUser の id と name を取得する
    id
    name
  }
}
  • 上記実行時のレスポンス(JSON)
{
  "data": {
    "currentUser": {
      "id": "dXNlci80Mgo=",
      "name": "foo",
    }
  }
}
ikeponikepon
  • スキーマ言語
    • type: リソースの単位
    • field: リソースの構成要素
      • field は引数を設定できる
type Query {
  user(id: ID!): User
}

id に対応した User を取得する、Userがない場合は null を返す

  • type のバリエーション
    • interface: Type と同様の方の一種
      • 対応する具体的なリソースを持たない抽象型
      • 複数 type に共通するフィールドを interface として抽出して使う
      • 関連性のある type の共通 field をまとめる
    • union
      • 関連性のない type をまとめた抽象型
      • 指定された複数の型のうち、いずれかの型を表す
    • enum
  • 全体に関わる要素
    • directive
    • description
ikeponikepon
  • Union
    • クエリで取得するときに必ずそれぞれの具象型ごとに conditional fragment でかき分けて取得する必要がある
type Entry {
  id: ID!
  title: String!
  content: String!
}

type Comment {
  id: ID!
  content: String!
}

union SearchResult = Entry | Comment;

typ Query {
  search(q: String!): [SearchResult!]!
}

この場合の query は以下のようになる

query {
  # 文字列 "foo" で検索する
  search(q: "foo") {
    __typename # 全ての型にデフォルトで提供されるメタデータ。ここでは "Entry" または "Comment"

    # Entryの場合
    ... on Entry { # ... on Type という記述が conditional fragment、SearchResult = Entry | Comment なので Entry と Comment の両方の記述が必要
      id
      title
      content
    }

    # Commentの場合
    ... on Comment {
      id
      content
    }
  }
}
ikeponikepon
  • Scalar

    • Int: 符号付き整数
    • Float: 浮動小数点数
    • String: 文字列
    • Boolean: 真偽値
    • ID: 一意なID(値としては String と同じ)
    • 新しく定義する場合は scalar を使う
      • scalar Date で Date 型を定義できる
  • Enum

    • scalar 型の一種
enum Boolean {
  true
  false
}
  • Directive
    • スキーマやクエリに対してメタデータを与えるための宣言
      • 処理系やツールによって解釈される
    • @deprecated: field が非推奨であることを示すための組み込み directive
type T {
  newName: String!
  oldField: String! @deprecated(reason: "Use `newField` instead.")
}
  • Description
    • type や field に対する説明
    • ツールから利用されるドキュメント
ikeponikepon
  • クエリ言語
    • オペレーション型
      • データ取得系のquery
      • データ更新系のmutation
      • pub/subモデルでサーバーサイドのイベントを受け取るsubscription
    • それぞれのオペレーション型ごとにルート型が必要
      • リソース表現ではなく、名前空間として利用される
  • Query
    • データ取得系のクエリ
    • ルート型は Query

スキーマ

# queryのルートオペレーション型であるQueryを定義する
type Query {
  # フィールドとしてはnon-nullableなUser型であるcurrentUserを持つ
  currentUser: User!
}

# User型のfieldはidとname
type User {
  id: ID!
  name: String!
}

クエリ

# クエリの種類は`query`で、この操作全体にはGetCurrentUserという名前をつける
query GetCurrentUser {
  # ここに型(ここではQuery)から取得したいfieldのリストを書く
  # なおクエリに書くfieldは "selection" という

  # queryのルート型QueryのフィールドcurrentUser: User! を要求する
  currentUser {
    # ここには User type のselectionを必要なだけ書く

    id # idはID!型 (ID型の実体は文字列)
    name # nameはString!型
  }
}

この query に対して GetCurrentUser という名前をつけているが、省略可能
query に限っては 最初の query というキーワードも省略できる

  • Query の命名
    • 名詞またはデータ取得を示す動詞であることが多い
    • 単にデータ取得であればgetやfindなどはつけずに名詞そのままにするのがGraphQL流といえる
ikeponikepon
  • Mutation
    • データ更新系のクエリ
    • ルートのオペレーション型: Mutation
    • mutationクエリには戻り値( selection set )の指定も必須で、たとえ戻り値を想定しないmutationでも必ず何かを書く必要がある
      • その場合、idやclientMutationIdを指定して、呼び出し元では単に無視する
        • clientMutationId: GraphQLではなくRelay Server Specificaionの仕様
    • mutationはQueryと違いMutation type直下に定義したものが全て
      • typeのfieldとしてmutationを定義することはできず、ネストはない
        • したがって、単体で意味の通る名前をつけることになる
    • mutationそれ自体は部分更新、つまりRESTful APIでいうところのPATCHに相当する仕様はありません
      • ひとつのmutationはスキーマ上では単にMutation typeのfieldであり、対応するリゾルバ関数がひとつあるだけ
        • つまり、mutationはシンプルなRPC(Remote Procedure Call)
      • もし部分更新を提供するのであれば、独立したひとつのmutationを定義することになります
        • 例えば、記事(article)のタイトル(title)だけを更新するmutationは、updateArticleTitleのようにする
  • ex: GitHub API v4において、あるリポジトリにstarをつけるmutationクエリ
mutation {
  # starrableIdに有効なIDを与えると本当にstarをつけてしまうので、無効なIDにしている
  addStar(input: { starrableId: "foo" }) {
    # 構文上は戻り値の指定が必須なので、不要なときは`clientMutationId`と書く
    clientMutationId
  }
}
  • Mutation の命名
    • 更新を示す動詞がいい
ikeponikepon
  • Variables
    • クエリとは別のパラメータ
    • variablesをクエリから参照するときは、クエリのオペレーション名のあとにパラメータリストを宣言
    • 型が厳密に一致してないといけない
      • nullable か否かもチェックされる
  • ex: GitHub API v4でorganizationの情報を得るとき
    • login(organization名; e.g. github)をパラメータにするときは、次のようにします。

クエリ

query GetOrganization($login: String!) {
  organization(login: $login) {
    name
    url
    description
  }
}

Variables

{
  "login": "github"
}
ikeponikepon
  • Fragment

    • クエリを分割して定義し、再利用しやすくするための機能
    • fragment fragment名 on 型 { フィールドのリスト } という構文で定義
    • ...ftagment名 で構文を展開
  • AST

    • GraphQL Schema language をパースした抽象構文木(AST)のこと
ikeponikepon
  • Relay Server Specification
    • https://relay.dev/docs/guides/graphql-server-specification/
    • GraphQL の拡張仕様
    • RelayはFacebookが開発しているGraphQLをReactで使うためのライブラリ
      • Relay Server SpecificationはRelayがGraphQL APIに求める規約という位置づけ
    • Relay Server SpecificationはFacebookが定めたものであることから、デファクトスタンダードな拡張仕様と考えていい
    • graphql-rubyが標準でサポート
    • 3つの仕様を定めている
      • Relay Node
        • リソースの再取得を統一的に行うための規約で、Node interfaceとQuery.node()からなります。
        • スキーマで表現すると次のようになります。
        • interface Node {
            id: ID!
          }
          
          type Query {
            node(id: ID!): Node
          }
          
          • id: すべての type のリソースにおいて一意であることが求められる
            • 実用的には base64encode("$tableName/$databaseId") を使うことが多い
      • Relay Connection
        • ページング可能なリスト型に関する規約
          • ページングはカーソルベースで行う
          • メタデータやリソースから独立した拡張データを入れるために、edgeとnodeという二重のラッパーtypeを要求します。
        • ex: User typeのConnectionであるUserConnectionの最小限のスキーマは次のようになります。
          • type User {
              id: ID!
              # 他のfieldは省略
            }
            
            type UserConnection {
              edges: [UserEdge]
              pageInfo: PageInfo!
            }
            
            type UserEdge {
              node: User!
              cursor: String!
            }
            
            type PageInfo {
              hasNextPage: Boolean!
              hasPreviousPage: Boolean!
              startCursor: String
              endCursor: String
            }
            
        • graphql-rubyはEdgeを省略したUserConnection.nodes: [User]を自動的に生成
          • Edge 型には賛否両論あり、graphql-ruby はこの形
      • Relay Mutation
        • mutationの引数と戻り値型に関する規約
        • 次の3つの規約からなっています
          • 入力はinputという引数名とし、その入力のためのtypeの接尾辞はInputとすること
          • 戻り値型はリソース型そのままではなくラッパー型をつくり、そのtypeの接尾辞はPayloadとすること
          • input型とpayload型はclientMutationId: Stringを含むこと
        • ex: GitHub API v4のMutation.addStar()を単純化すると次のようなスキーマになる
          • type Mutation {
              addStar(input: AddStarInput!): AddStarPaload!
            }
            
            type AddStarInput {
              starrableId: ID!
              clientMutationId: String
            }
            
            type AddStarPayload {
              starrable: Starrable!
              clientMutationId: String
            }
            
            type Starrable {
              id: ID!
              # 他のfieldは省略
            }
            
            • input型をtypeとしてまとめているのは、次のように単一の$inputで入力パラメータ全体を渡すため
              mutation AddStar($input: AddStarInput!) {
                addStar(input: $input) {
                  clientMutationId
                }
              }
              
            • パラメータが多いケースだとひとつひとつ渡すと煩雑なため、ひとつのオブジェクトとして渡すほうがシンプルになります。
            • 戻り値もリソース型そのままではなくpayload型をラッパーとすることで、将来拡張することになっても互換性を壊さずに済みます。
ikeponikepon
  • graphql-ruby
    • 大きな特徴として、GraphQLのスキーマを直接は書かない
      • そのかわり、Rubyコードでスキーマと実装を定義していく方式を採用
      • GraphQLスキーマは、もし必要であればgraphql-rubyの機能で生成可能
  • クエリ実装
    • bin/rails generate graphql:object ArticleType で Article type を定義
    • types/article_type.rb が生成される
      module Types
        class ArticleType < Types::BaseObject
        end
      end
      
    • ここに必要なフィールドを追加
        module Types
          class ArticleType < Types::BaseObject
      +     field :id, ID, null: false
      +     field :name, String, null: false
      +     field :content, String, null: false
      +     field :created_at, GraphQL::Types::ISO8601DateTime, null: false
          end
        end
      
    • graphql-ruby はデフォルトでリゾルバを用意している
      • 本来なら resovler をフィールドごとに定義する必要がある
        module Types
          class ArticleType < Types::BaseObject
            field :id, ID, null: false, resolve: -> (article, _args, _context) { article.id }
            field :name, String, null: false, resolve: -> (article, _args, _context) { article.name }
            # ほかは省略
          end
        end
        
    • Query.articles を定義
      • クエリから Types::ArticleType にアクセスできるようにする
          module Types
            class QueryType < Types::BaseObject
        +     field :articles, Types::ArticleType.connection_type, null: false, resolve: -> (_object, _args, _context) do
        +       Article.order(id: :desc)
        +     end
            end
          end
        
      • Types::ArticleType.connection_type は、graphql-rubyが生成する ArticleConnection type
        • ArticleType を要素に持つRelay Connectionに準拠したtype
    • GraphQL スキーマを出力
      • bin/rails runner 'puts GraphqlBlogSchema.to_definition'
    • クエリの実行
      query {
        articles {
          edges {
            node {
              id
              name
              content
              createdAt
            }
          }
        }
      }
      
ikeponikepon
  • mutation の実装
    • bin/rails generate graphql:mutation CreateArticle でmutation に必要なファイルを生成

      module Mutations
        class CreateArticle < GraphQL::Schema::RelayClassicMutation
          # TODO: define return fields
          # field :post, Types::PostType, null: false
      
          # TODO: define arguments
          # argument :name, String, required: true
      
          # TODO: define resolve method
          # def resolve(name:)
          #   { post: ... }
          # end
        end
      end
      
      • 上記コマンドにより Types::Mutation にも field が追加される
        • mutation fieldに関する全ての情報はmutationクラスにあるため、Types::MutationTypeには宣言だけあれば十分
           module Types
             class MutationType < Types::BaseObject
          +    field :createArticle, mutation: Mutations::CreateArticle
             # ...
          
    • mutation の実装コード

      module Mutations
        class CreateArticle < GraphQL::Schema::RelayClassicMutation
      
          argument :name, String, required: true
          argument :content, String, required: true
      
          field :article, Types::ArticleType, null: false
      
          def resolve(name:, content:)
            article = Article.new(name: name, content: content)
            article.save!
            { article: article }
          end
        end
      end
      
      • Mutations::CreateArticleで定義しているtypeは、Mitation.createArticleの引数であるCreateArticleInput typeと、戻り値であるCreateArticlePayload type
        • bin/rails runner 'puts GraphqlBlogSchema.to_definition' で正しく定義できているか確認できる
      • これを実行するクエリ
        mutation {
          createArticle(input: { name: "hello from graphql api", content:"yey!"}) {
            article {
              id
              name
              content
              createdAt
            }
          }
        }