はじめに
GraphQLに関するOSSにコミットする中で、それ以前のGraphQL APIの開発では気づかなかった、GraphQLの通なポイントを3つのセリフにまとめてみました。知ったかする際に役に立つと思うので、ぜひご活用ください!
1. GraphQLって9割がたフィールドだよね
GraphQLにはQuery, Mutation, Subscriptionの3つがある、というのはご存知だと思います。だいたい以下のような役割分担です。
- Query: データ取得用
- Mutation: データ変更用
- Subscription: サーバからのプッシュ用
例えば「ユーザ一覧を取得するためのusers
というQuery」を作ったり「ユーザを新規登録するためのcreateUser
というMutation」を作ったりするイメージです。
ここで、users
やcreateUser
をQueryやMutationと呼んでも間違いではないのですが、正確には「Queryというルートオブジェクトのフィールドであるusers
」や「MutationというルートオブジェクトのフィールドであるcreateUser
」だったりします。
つまり「ユーザ一覧を取得するためのusers
というQueryを作ろう」は「ユーザ一覧を取得するためのusers
フィールドをQueryオブジェクトに追加しよう」と言い換えたほうが、GraphQL的には正確で、ちゃんと理解しているんだぞ感をかもし出すことができます。
もちろん、単にめんどくさがれる可能性が高いです。
2. インプットにUnion型がないのは残念だよね
GraphQLのインプットにはUnion型がありません。補足しておくと、Union型はA | B
のように、AまたはBのどちらかの型を受け取れる型、みたいなやつです。
なお、アウトプットにはUnion型があります。以下のような感じで、直感的にUnionを定義することができます。しかしこれをインプットとして使おうとすると怒られてしまいます。
union Media = Book | Movie
この仕様はリッチな型定義に慣れた開発者に評判が悪かったのか、インプットでもUnion型をサポートしようよというRFCが立ちました。その後紆余曲折あり@oneOf
というディレクティブが導入される予定になっています。
これまでのGraphQLでは、型的に堅牢であろうとすると、例えば以下のように、IDでユーザを取得するためuserByID
とメールアドレスで取得するためのuserByEmail
を別々に定義することを求められる場合がありました。
type Query {
userByID(id: ID!): User
userByEmail(email: String!): User
}
これを@oneOf
を使うことで、以下のように1つのuser
というQuery(正確にはQueryオブジェクトのフィールド)にまとめることができます。@oneOf
によってid
またはemail
のどちらか1つのみが指定されることが保証されるため、型的にストイックな方も満足できる仕様になっていると思われます。
type Query {
user(by: UserByInput!): User
}
input UserByInput @oneOf {
id: ID
email: String
}
参考: Coming Soon To GraphQL: The oneof Input Object
3. ResolveInfoを使いこなして初めてGraphQLに価値が生まれるよね
ResolveInfoは(正式にはGraphQLResolveInof)は、多少込み入った処理をする際に必要になってくる奴だと思ってください。
例えば以下のクエリを例にResolveInfoの利用例を考えてみましょう。このクエリは、各ユーザのid
とname
、さらに各ユーザに紐づく投稿(posts)のid
とtitle
を取得するためのクエリです。
query {
users {
id
name
posts {
id
title
}
}
}
GraqhQLではクエリで指定された各フィールドについて、それぞれのフィールドに設定されたresolverを順々に呼び出すことで、最終的な戻り値を生成します。上記のクエリの場合、実際の処理の流れは以下のようになります。
- ルートオブジェクトであるQueryの
users
フィールドのresolverが呼ばれる - (1)のresolverの実行結果がN件(N > 0)のデータを返す場合、各データについて
id
、name
、posts
フィールドのresolverがN回呼ばれる -
posts
フィールドのresolverの実行結果がM件(M > 0)のデータを返す場合、各データについてid
、title
フィールドのresolverがM回呼ばれる
各フィールドのresolverのデフォルト(=resolverが未設定の場合)の挙動は、親フィールドのresolverが返した値(オブジェクト)から、自身のフィールド名のプロパティを返す、というものです。例えば(1)で返される値が[{ id: 1, name: "Jon", posts: [] }]
だった場合、(2)のid
フィールドのデフォルトresolverは1
を返し、name
フィールドは"Jon"
、posts
フィールドは[]
を返します。
つまり、大本のusers
フィールドのresolverが、指定されたフィールドの値をすべて生成して返すことができれば、それ以外のフィールドはデフォルトresolverに任せることができます。今回の例であれば、users
フィールドのresolver内で、上記のGraphQLクエリをもとに、以下のようなSQLなどを生成しDBから値を取得することができれば良さそうです。
SELECT
User.id,
User.name,
Post.id as postId,
Post.title
FROM
User INNER JOIN Post ON User.id = Post.user_id
このような、GraphQLクエリからSQLへの変換の仕組みが、例のHasuraやPostGraphileで実装されているものです(たぶん)。そして、そのために必要なのがResolveInfoになります。ResolveInfoからクエリで指定されたフィールドを取得し、そこからSQLを生成する、という流れです。
それ以外にも、各フィールドに処理を任せるより、ResolveInfoを用いてルートのフィールドでまとめて処理をしたほうがパフォーマンスが出るなどの理由で、argsのバリデーションや権限制御などの処理で、ResolveInfoは活躍します。
実際にResolveInfoからどんな値が取得できるのか、詳しくはGraphQL.jsの型定義をご参照ください。
まとめ
- GraphQLって9割がたフィールドだよね
- インプットにUnion型がないのは残念だよね
- ResolveInfoを使いこなして初めてGraphQLに価値が生まれるよね
さり気なくこの3フレーズを使うことで、皆さんの単価などが上がることを願っています。
もし興味が湧いた方は、この記事のきっかけとなったOSSとGraphQLに関するチュートリアルも見てみてください。