graphql を実行しながら GraphQLSchema と resolver、rootValue まわりの評価の仕組みを考える
の続きです。前回は graphql と graphql-tools について、結構な量のメソッドやクラスを駆け足で読み解きながら、なんとなく GraphQL 界隈のパッケージの局所像を掴んだのでした。
今回はもう少しまったりと graphql パッケージを触りながら GraphQLSchema
、resolver
、rootValue
について理解を深めていきたいと思います。
graphql を実行しながら評価の仕組みを体感する
https://www.npmjs.com/package/graphql にある例をベースに進めていきます。
import { graphql, GraphQLSchema, GraphQLObjectType, GraphQLString } from 'graphql' const schema = new GraphQLSchema({ query: new GraphQLObjectType({ name: 'RootQueryType', fields: { hello: { type: GraphQLString, resolve() { // return 'world' // <- ココと } // } } }) }) const rootValue = { hello: 'world in the rootValue', // <- ココをいじります } graphql(schema, `{ hello }`, rootValue).then(console.log, console.error) // => { data: { hello: 'world' } }
schema
と rootValue
を定義して graphql(schema, '{ hello }', rootValue)
で実行する、という単純なコードです。
前回得られた(推測混じりの) GraphQL の各フィールドの値解決のフローを改めて書いておきます。
- フィールドに
resolver
が定義されている場合はそれが実行される - 定義されていない場合は
defaultFieldResolver
の定義によりsource[fieldName']
が返されるsource
の値はresolver
の引数として与えられ、それは自身の直上のオブジェクトを指すGraphQLSchema
自身のsource
となるのがrootValue
である
上のコードの結果 { data: { hello: 'world' } }
を見てみると、hello
フィールドに定義された resolver
の評価値 'world'
が rootValue
の値に優先されて取得されているのがわかります。
このコードを起点に resolver
と rootValue
を書き換えながら振る舞いを観察していきます。
rootValue なしで動かしてみる
小手調べに rootValue
なしで実行してみると
-graphql(schema, `{ hello }`, rootValue).then(console.log, console.error) +graphql(schema, `{ hello }`).then(console.log, console.error)
問題なく { data: { hello: 'world' } }
を返します。
動かしながら graphql-tools の makeExecutableSchema
で生成される GraphQLSchema
はこれなのかな...と思いました。
つまり、resolver
が仕込まれているので rootValue
無しで実行可能な GraphQLSchema
という意味なのでは、という予想です。
resolver なしで動かしてみる
どんどんいきます。resolver
を消してみると
fields: { hello: { type: GraphQLString, - resolve() { - return 'world' - } } }
{ data: { hello: 'world in the rootValue' } }
と rootValue
の値が反映された結果が帰ってきます。
resolver も rootValue もなしで動かしてみる
上の resolver
を取り除いた状態から更に rootValue
の値を undefined
や {}
にすると { data: { hello: null } }
が帰ってきます。
resolver
もされず defaultFieldResolver
が参照する先の rootValue
にも自身のフィールドが存在しない値は、当然ですが帰ってきませんね。
意外だったのはエラーも上がらないことでした。
graphql
実行時のエラーハンドリングは一つのテーマになりそうですね。
rootValue 内で関数を定義する
前回 defaultFieldResolver
を眺めていて気づいたのが関数も受け取ることができ、そして引数を渡され評価されるという点でした。
つまりこういう関数フィールドを持った rootValue
も書けるということですね。
const schema = new GraphQLSchema({ query: new GraphQLObjectType({ name: 'RootQueryType', fields: { hello: { type: GraphQLString, - resolve() { - return 'world' - } } } }) }) const rootValue = { - hello: 'world in the rootValue' + hello() { return 'world in the rootValue' } }
これでも予想通り { data: { hello: 'world in the rootValue' } }
と評価されます。
rootValue
にも関数が書けるとなると resolver
との使い分けは如何に、という疑問が新たに浮かびます。
rootValue の関数フィールドと resolver に渡される引数の違い
コードの rootValue
の関数フィールドと GraphQLSchema
のフィールド定義の resolver
の両方で関数が定義できることがわかりましたが、何が違うのでしょうか。
この部分、場合によってはDBへのクエリやリモートリソースからの fetch 処理に置き換わる部分なので、両者の使い分けには興味があります。
評価時に渡される引数の違いについて見ていきます。
フィールド定義の resolver 関数
前回も軽く触れましたが graphql/type | API Reference より、フィールド定義の resolver
関数は以下のような引数で実行されます。
type GraphQLFieldResolveFn = ( source?: any, args?: {[argName: string]: any}, context?: any, info?: GraphQLResolveInfo ) => any
rootValue 内の関数フィールド
defaultFieldResolver 内に関数評価を行うコードが存在します。
source[info.fieldName](args, context, info)
値のつなぎこみの部分を見るに、引数名と中身のデータはフィールド定義の resolver
のものと同一みたいですね。
両者の違い
唯一の違いが rootValue
の関数には source
が存在しない、というところですね。
context
とinfo
は内容が重そうなので一旦置いておきますargs
は GraphQL のフィールドは関数のように変数を受け付けるので、クエリで指定された引数が渡される場所ですね
source
を引数に受け取れないということは例えば User
オブジェクトのフィールド定義を行うとして、User.friends
の人数を参照するような User.friendsNum
フィールドを定義することは rootValue
内の関数では不可能、ということですね。User.id
を参照して外部にクエリを投げるような処理も同様に行えない、ということになりますね。
なので、そういう処理が書きたければおとなしく resolver
を書く必要があり、それには graphql が提供する機能だけでは大変なので graphql-tools の makeExecutableSchema
を使うと楽、という感じになるでしょうか。
おお、ちょっとすっきりした気がします。
まとめ
現在、ネット上にサンプルが豊富に存在するということもあり、いきなりサーバにバインディングされたコードから GraphQL に入ってくることも多いと思います。 実際私もその手合いの一人でしたが、生の graphql の処理系に触れることで、より肌感覚的な理解を行えた気がします。
なにより、いろんな条件で値を変えて動作の仕組みを推測するのには、サーバ経由でクエリを叩くよりも格段に楽ですし、ぜひオススメしたい方法です。