Open13
GraphQL
GraphQLは、Facebookが開発しているWeb APIのための規格
「クエリ言語」と「スキーマ言語」からなる
- クエリ言語
- query: データ取得系
- mutation: データ更新系
- subscription: サーバーからのイベント通知
- スキーマ言語
- GraphQL API の仕様を記述するための言語
- スキーマの例
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",
}
}
}
- スキーマ言語
- type: リソースの単位
- field: リソースの構成要素
- field は引数を設定できる
type Query {
user(id: ID!): User
}
id に対応した User を取得する、Userがない場合は null を返す
- type のバリエーション
- interface: Type と同様の方の一種
- 対応する具体的なリソースを持たない抽象型
- 複数 type に共通するフィールドを interface として抽出して使う
- 関連性のある type の共通 field をまとめる
- union
- 関連性のない type をまとめた抽象型
- 指定された複数の型のうち、いずれかの型を表す
- enum
- interface: Type と同様の方の一種
- 全体に関わる要素
- directive
- description
- 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
}
}
}
-
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 に対する説明
- ツールから利用されるドキュメント
- クエリ言語
- オペレーション型
- データ取得系の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流といえる
- Mutation
- データ更新系のクエリ
- ルートのオペレーション型: Mutation
- mutationクエリには戻り値( selection set )の指定も必須で、たとえ戻り値を想定しないmutationでも必ず何かを書く必要がある
- その場合、idやclientMutationIdを指定して、呼び出し元では単に無視する
- clientMutationId: GraphQLではなくRelay Server Specificaionの仕様
- その場合、idやclientMutationIdを指定して、呼び出し元では単に無視する
- mutationはQueryと違いMutation type直下に定義したものが全て
- typeのfieldとしてmutationを定義することはできず、ネストはない
- したがって、単体で意味の通る名前をつけることになる
- typeのfieldとしてmutationを定義することはできず、ネストはない
- mutationそれ自体は部分更新、つまりRESTful APIでいうところのPATCHに相当する仕様はありません
- ひとつのmutationはスキーマ上では単にMutation typeのfieldであり、対応するリゾルバ関数がひとつあるだけ
- つまり、mutationはシンプルなRPC(Remote Procedure Call)
- もし部分更新を提供するのであれば、独立したひとつのmutationを定義することになります
- 例えば、記事(article)のタイトル(title)だけを更新するmutationは、updateArticleTitleのようにする
- ひとつのmutationはスキーマ上では単にMutation typeのfieldであり、対応するリゾルバ関数がひとつあるだけ
- ex: GitHub API v4において、あるリポジトリにstarをつけるmutationクエリ
mutation {
# starrableIdに有効なIDを与えると本当にstarをつけてしまうので、無効なIDにしている
addStar(input: { starrableId: "foo" }) {
# 構文上は戻り値の指定が必須なので、不要なときは`clientMutationId`と書く
clientMutationId
}
}
- Mutation の命名
- 更新を示す動詞がいい
- 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"
}
-
Fragment
- クエリを分割して定義し、再利用しやすくするための機能
-
fragment fragment名 on 型 { フィールドのリスト }
という構文で定義 -
...ftagment名
で構文を展開
-
AST
- GraphQL Schema language をパースした抽象構文木(AST)のこと
- 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")
を使うことが多い
- 実用的には
- id: すべての type のリソースにおいて一意であることが求められる
- 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型をラッパーとすることで、将来拡張することになっても互換性を壊さずに済みます。
- input型をtypeとしてまとめているのは、次のように単一の$inputで入力パラメータ全体を渡すため
-
- Relay Node
- graphql-ruby
- 大きな特徴として、GraphQLのスキーマを直接は書かない
- そのかわり、Rubyコードでスキーマと実装を定義していく方式を採用
- GraphQLスキーマは、もし必要であればgraphql-rubyの機能で生成可能
- 大きな特徴として、GraphQLのスキーマを直接は書かない
- クエリ実装
-
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
- 本来なら resovler をフィールドごとに定義する必要がある
- 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 } } } }
-
- 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 fieldに関する全ての情報はmutationクラスにあるため、Types::MutationTypeには宣言だけあれば十分
- 上記コマンドにより Types::Mutation にも field が追加される
-
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 } } }
- Mutations::CreateArticleで定義しているtypeは、Mitation.createArticleの引数であるCreateArticleInput typeと、戻り値であるCreateArticlePayload type
-