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

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

GraphQL の主要ライブラリと resolver、schema、rootValue についてざらっと眺めてみる

(=˘ ꒳ ˘=) resolverrootValue って何が違うんだろう... makeExecutableSchema って何なんだろう...

...な記憶も新しく、むしろ現在進行形で混乱中ですが、めげずに公式サイト等眺めながら理解を進めていきたいと思います。

GraphQL 系パッケージまとめ

まず、前提知識の整理として主な GraphQL 系のパッケージをまとめていきたいと思います。

GraphQLのHTTPサーバ用ミドルウェアについては http://lightbulbcat.hatenablog.com/entry/2018/01/26/014846 でまとめております。

  • graphql
    • Facebook 製の参照実装かつデファクトスタンダードな GraphQL のコアライブラリです
    • GraphQLSchema, GraphQLObjectType などの基本的なデータ構造を提供します
    • buildSchema などのユーティリティが含まれます
    • 以下、小文字の graphql はこのパッケージを指すものとします
  • graphql-tools
    • Apollo 製のユーティリティです
    • makeExecutableSchema が含まれます
    • テスト用のモックを生成できたりもします

このあたり、コードを書いていると、どのメソッドがどちらのパッケージに属しているかで混乱したりします。また graphql-tools を使わない実装例も存在するので、実装の方法が複数あるというところでも混乱した記憶があります。

GraphQLSchema を作成する主な方法

schema としてサーバのミドルウェアに渡すようなオブジェクトは graphql で定義された GraphQLSchema というクラスのオブジェクトらしいのですが、それを生成する手段として、現状よく見かけるのは

  • graphql の buildSchema を使う
  • graphql-tools の makeExecutableSchema を使う
  • new GraphQLSchema でゴリゴリ書く

の3通りですね。それぞれ軽く見ていきましょう。

apollo-tools のお作法にのっとり、以下の例では GraphQL Schema Language で書かれたスキーマ定義文字列が typeDefs としてスコープ内に定義されているものとしています。

graphql: buildSchema

const schema = buildSchema(typeDefs)

graphql-tools: makeExecutableSchema

const schema = makeExecutableSchema({ typeDefs, resolvers });

graphql: GraphQLObjectType

const schema = new GraphQLSchema({
  query: new GraphQLObjectType({
    name: 'RootQueryType',
    fields: {
      hello: {
        type: GraphQLString,
        resolve() {
          return 'world';
        }
      }
    }
  })
});

こうしてみると対応箇所が見えてきて、気付きや疑問点が湧いてきます。

  • GraphQLSchema に含まれる GraphQLObjectType の各フィールドはそれぞれ resolver を持つ
    • 対して、定義から推測できるように buildSchema で生成された GraphQLSchema には resolver は含まれない
    • ソース を見ると生成されるスキーマresolver は含まれないので実行時にデフォルトの resolver が使われるとある
    • これに値を与えるのが後述する、クエリ実行時の rootValue
  • makeExecutableSchema についての疑問(主にソースコードが追いきれず...)
    • 生成される schema は GraphQLSchema と互換性のあるものなのか
    • resolvers は各対応する GraphQLObjectType に分配されるようなイメージであっているか
    • 上の仮定が正しければ makeExecutableSchema の立ち位置は、生成される GraphQLSchema 以下の GraphQLObjectTyperesolver を注入することのできる buildSchema と理解できますね

graphql の値解決の仕組みと rootValue

http://graphql.org/graphql-js/#writing-code にある通り graphql はクエリに対する値を解決するために root と呼ばれる値を利用できます。

graphql(schema, '{ hello }', root).then((response) => {
  console.log(response);
});

ここで出てくる root というのがミドルウェアに渡している rootValue 相当のものだと思います。

graphql の resolver 定義

graphql-tools の makeExecutableSchema を利用したことのある方なら見覚えがあると思いますが、各フィールドに対するリゾルバは graphql のドキュメント にある通り、以下のような形をしています。

type GraphQLFieldResolveFn = (
  source?: any,
  args?: {[argName: string]: any},
  context?: any,
  info?: GraphQLResolveInfo
) => any

この source という引数には自身の直上の階層のオブジェクトが与えられ、例えば User 型のオブジェクトのフィールドの resolver として定義されていれば親の User オブジェクトとなります。

graphql の defaultFieldResolver

ソースコードを追いかけてみるとデフォルトで使用される resolver では source に対して自身のフィールド名で参照している雰囲気でした。

export const defaultFieldResolver: GraphQLFieldResolver<any, *> = function(
  source,
  args,
  context,
  info,
) {
  // ensure source is a value for which property access is acceptable.
  if (typeof source === 'object' || typeof source === 'function') {
    const property = source[info.fieldName];
    if (typeof property === 'function') {
      return source[info.fieldName](args, context, info);
    }
    return property;
  }
};

graphql/graphql-js/src/execution/execute.js

まとめ

ここまで出てきた材料から推測するに、各クエリ実行時の graphql の値解決は...

  • 実行時に与えられた rootValue に対して、クエリに従って各フィールドを求めていく
  • 各フィールドは source (自身の直上のオブジェクト)を参照しながら resolver により自身の値を決定する
    • デフォルトの resolver の定義は source[自身のフィールド名]
    • これは resolver の定義により自由に振舞を変えられる
  • そしてその resolver が定義された GraphQLSchema を作るには...
    • graphql-tools の makeExecutableSchema を利用すると便利
    • graphql の GraphQLObjectType を頑張って使う方法もある
    • graphql の buildSchema では各フィールドへの resolver の定義は不可能

推測の域を出ない箇所もありますが、調べれば調べるほど宿題が増えていくので、今回はここまでの仮定を立てられただけでも良しとします。

間違いなどありましたら、ご指摘いただけるとありがたいです。