本稿は公式サイト「Execution」にもとづく、GraphQLの構文検証の考え方についての解説です。ドキュメントの邦訳ではなく、日本語で説明し直しました。原文から省いた部分もあり、逆にわかりにくいところは補っています。なお、GraphQL公式サイトのコード例は、インタラクティブな環境です。コードを書き替えて結果が確かめられますので、ぜひ試してみてください。
GraphQLクエリは検証されたあと、GraphQLサーバーにより実行されます。返されるのはクエリの形状(shape)にしたがった結果で、通常はJSONです。
GraphQLは型システムなしではクエリを実行できません。クエリの実行を説明するために、つぎのような型システムの例を使いましょう。
type Query {
human(id: ID!): Human
}
type Human {
name: String
appearsIn: [Episode]
starships: [Starship]
}
enum Episode {
NEWHOPE
EMPIRE
JEDI
}
type Starship {
name: String
}
ルートフィールドとリゾルバ(resolver)
GraphQLサーバーの最上位にあるのは、GraphQL APIへのすべてのエントリーポイントを示す型です。ルート型あるいはクエリ型と呼ばれます。
前掲の例では、クエリ型はhuman
というフィールドを提供し、id
という引数を受け取ります。このフィールドに対して、データベースにアクセスし、Human
オブジェクトを構築して返すのがリゾルバ(resolver)関数です。
Query: {
human(obj, args, context, info) {
return context.db.loadHumanByID(args.id).then(
userData => new Human(userData)
)
}
}
ここで例として用いるのはJavaScriptです。けれど、GraphQLサーバーはさまざまな異なる言語で構築できます。リゾルバ関数が受け取る引数は4つです。
-
obj
- 以前のオブジェクト。ルートのクエリ型フィールドにはあまり使われない。 -
args
- GraphQLクエリでフィールドに渡される引数。 -
context
- 各リゾルバに与えられる値。つぎのような重要なコンテクスト情報をもつ。- 現在ログインしているユーザー
- データベースへのアクセスなど
-
info
- スキーマの詳細と同じく、現在のクエリにかかわるフィールド固有の情報をもつ値。- 詳しくは、「graphql/type」の「GraphQLObjectType」参照。
非同期リゾルバ
リゾルバ関数で何が行われているのかをご説明します。
human(obj, args, context, info) {
return context.db.loadHumanByID(args.id).then(
userData => new Human(userData)
)
}
データベースへのアクセスを得るために用いられるのがcontext
です。GraphQLクエリの引数として渡されたid
により、データベースからユーザーのデータがロードされます。データベースからの読み込みは非同期処理です。そのため、JavaScriptでは非同期の値を扱うPromise
が返ります。同じ考えは多くの言語にあり、Futures
、Tasks
、Deferred
などとも呼ばれる機能です。データベースから戻ってきたら、新しいHuman
オブジェクトを構築して返すことができます。
リゾルバ関数はPromise
を認識していなければなりません。GraphQLクエリはそうではないことにご注意ください。ただ、human
フィールドが何かを返すとみなし、ただname
を問うだけです。実行中、GraphQLはPromise
、Futures
、Tasks
が完了するのを待ちます。そのあと、最適な並行性で実行されるのです。
単純なリゾルバ
Human
オブジェクトが使えるようになると、GraphQLは要求されたフィールドで実行を続けられます。
Human: {
name(obj, args, context, info) {
return obj.name
}
}
GraphQLサーバーは、型システムの支えにより、つぎに何をすべきか決めるのです。Human
フィールドがまだ何も返さなくても、GraphQLはつぎにやることはHuman
型のフィールドの解決だとわかります。型システムから、human
フィールドが返すのはHuman
だと知らされているからです。
ここでは、name
の解決はとても簡単です。name
リゾルバ関数が呼び出されると、引数obj
にnew Human
オブジェクトを受け取ります。これは、前のフィールドから返されたオブジェクトです。そして、Human
オブジェクトはname
プロパティをもち、直接読み込んで返すことができるとわかります。
実際、多くのGraphQLライブラリでは、このような単純なリゾルバは省いてしまえます。フィールドにリゾルバが与えられていない場合、同じ名前のプロパティが読み込まれて返されると仮定すればよいからです。
スカラー強制(coercion)
name
フィールドが解決される間に、appearsIn
とstarships
のふたつのフィールドも同時に解決できます。ただ、appearsIn
フィールドは簡単なリゾルバをもっているかもしれません。
Human: {
appearsIn(obj) {
return obj.appearsIn // 戻り値: [ 4, 5, 6 ]
}
}
はじめに掲げた型システムによれば、appearsIn
はあらかじめ決められた値を返す列挙型のリストです。ところが、リゾルバ関数の戻り値のリストは数値を要素としています。たしかに結果を見ると、返されるのは正しい列挙型の値です。
これはスカラー強制(coercion)と呼ばれる機能の例です。型システムは何が期待されているかを知っています。そこで、リゾルバ関数から返された値を APIの規約にしたがって変換するのです。サーバーで列挙型が定められていれば、内部的には4、5、6などの数字を用いていても、GraphQLの型システムは列挙型の値として表します。
リストリゾルバ
リストを返すフィールドの扱いは、前項のappearsIn
で垣間見ました。返されるのは列挙型のリストだと型システムが認識したので、各項目は強制的に適切な列挙型値に変換されたのです。今度は、やはりリストが返されるstarships
フィールドを採り上げましょう。
Human: {
starships(obj, args, context, info) {
return obj.starshipIDs.map(
id => context.db.loadStarshipByID(id).then(
shipData => new Starship(shipData)
)
)
}
}
このフィールドのリゾルバが返すのはPromise
単体ではなく、Promise
のリストです。Human
オブジェクトは、操縦する宇宙船のid
のリストをもちます。けれど、それらは実際のStarship
オブジェクトを得るために必要なのです。
GraphQLはこれらのPromise
をすべて同時に待ち、そのあと処理を続けます。そして、リストに残ったオブジェクトがあればふたたび同時に処理を続け、各項目のname
フィールドを読み込むのです。
結果の生成
各フィールドが解決されると、結果の値はキー-値のマップに配置されます。キーはフィールド名(またはエイリアス)、解決された値が値です。クエリの最下層の末端フィールドから、ルートのクエリ型の大もとのフィールドまでずっと続きます。これらをまとめたのが、もとのクエリを反映した構造です。要求したクライアントに(通常はJSONとして)送信できるようになります。
クエリからリゾルバ関数が、どのように結果を生成するか確かめましょう。
{
human(id: 1002) {
name
appearsIn
starships {
name
}
}
}
{
"data": {
"human": {
"name": "Han Solo",
"appearsIn": [
"NEWHOPE",
"EMPIRE",
"JEDI"
],
"starships": [
{
"name": "Millenium Falcon"
},
{
"name": "Imperial shuttle"
}
]
}
}
}
シリーズGraphQLの基本
「GraphQL: クエリ(queries)と変更(mutations)」
「GraphQL: スキーマと型」
「GraphQL: 検証(validation)」
「GraphQL: 実行」
「GraphQL: イントロスペクション(introspection)」