きみはねこみたいなにゃんにゃんなまほう

ねこもスクリプトをかくなり

graphql を実行しながら GraphQLSchema と resolver、rootValue まわりの評価の仕組みを考える

lightbulbcat.hatenablog.com

の続きです。前回は graphqlgraphql-tools について、結構な量のメソッドやクラスを駆け足で読み解きながら、なんとなく GraphQL 界隈のパッケージの局所像を掴んだのでした。

今回はもう少しまったりと graphql パッケージを触りながら GraphQLSchemaresolverrootValue について理解を深めていきたいと思います。

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' } }

schemarootValue を定義して graphql(schema, '{ hello }', rootValue) で実行する、という単純なコードです。

前回得られた(推測混じりの) GraphQL の各フィールドの値解決のフローを改めて書いておきます。

  • フィールドに resolver が定義されている場合はそれが実行される
  • 定義されていない場合は defaultFieldResolver の定義により source[fieldName'] が返される
    • source の値は resolver の引数として与えられ、それは自身の直上のオブジェクトを指す
    • GraphQLSchema 自身の source となるのが rootValue である

上のコードの結果 { data: { hello: 'world' } } を見てみると、hello フィールドに定義された resolver の評価値 'world'rootValue の値に優先されて取得されているのがわかります。

このコードを起点に resolverrootValue を書き換えながら振る舞いを観察していきます。

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 が存在しない、というところですね。

  • contextinfo は内容が重そうなので一旦置いておきます
  • args は GraphQL のフィールドは関数のように変数を受け付けるので、クエリで指定された引数が渡される場所ですね

source を引数に受け取れないということは例えば User オブジェクトのフィールド定義を行うとして、User.friends の人数を参照するような User.friendsNum フィールドを定義することは rootValue 内の関数では不可能、ということですね。User.id を参照して外部にクエリを投げるような処理も同様に行えない、ということになりますね。

なので、そういう処理が書きたければおとなしく resolver を書く必要があり、それには graphql が提供する機能だけでは大変なので graphql-tools の makeExecutableSchema を使うと楽、という感じになるでしょうか。

おお、ちょっとすっきりした気がします。

まとめ

現在、ネット上にサンプルが豊富に存在するということもあり、いきなりサーバにバインディングされたコードから GraphQL に入ってくることも多いと思います。 実際私もその手合いの一人でしたが、生の graphql の処理系に触れることで、より肌感覚的な理解を行えた気がします。

なにより、いろんな条件で値を変えて動作の仕組みを推測するのには、サーバ経由でクエリを叩くよりも格段に楽ですし、ぜひオススメしたい方法です。