GraphQL: gqlgenで指定されたFieldに応じてresolverの実装を変える

GraphQLのいいところとして、クライアントがリクエストごとに欲しいフィールドを明示するので、サーバ側はリクエスト毎にレスポンス返却を最適化しやすい、という点が挙げられると思います。

gqlgenというGoのGraphQLライブラリがあり、これを使って実際に要求されるFieldに応じて、レスポンスの作成 (gqlgenではresolverという) を最適化するにはどうしたらいいか?
結論から言うと、 `graphql.ResolverContext` にクライアントが求めるFieldが入っているので、それを見たらよいです。

gqlgenでの実装例ですが、だいたいのライブラリはこういうかんじだろうと思います。ScalaのSangriaが似たAPIを供えていることは確認しています。


たとえばデータストアにはMarkdownはてな記法で書かれた本分を保存しておき、APIの応答としてはそれを展開したHTML断片を返却したい、というケース。
Markdownはてな記法の展開はそこそこコストがかかりますし、そもそも本分データは他のカラムに比べてデータ量が多く、クエリ数が増えるとDBとアプリケーションの間の転送量も馬鹿にならないので節約したいです。

gqlgenでは `graphql.ResolverContext.Field.Selections` にクライアントが求めるFieldが渡されるので、これの名前 (`Name`) の一致を見て分岐させることができます

schema.graphql:

type Query {
  article(slug: String!): Article
}

type Article {
  title: String!
  slug: String!
  formattedBody: String!
}

resolver.go:

import (
	"context"

	"github.com/99designs/gqlgen/graphql"
	"github.com/vektah/gqlparser/ast"
)

func (r *queryResolver) Article(ctx context.Context, slug string) (*Article, error) {
	var (
		article *Article
		err error
	)
	if shouldLoadBody(graphql.GetResolverContext(ctx)) {
		article, err = r.resolveArticleWithBody(ctx, slug)
		if err != nil {
			return nil, err
		}
	} else {
		article, err = r.resolveArticle(ctx, slug)
		if err != nil {
			return nil, err
		}
	}

	return article, nil
}

func shouldLoadBody(resCtx *graphql.ResolverContext) bool {
	for _, sel := range resCtx.Field.Selections {
		switch sel := sel.(type) {
		case *ast.Field:
			if sel.Name == "formattedBody" {
				return true
			}
		}
	}
	return false
}

`shouldLoadBody` が肝です。 `resolveArticleWithBody` / `resolveArticle` は省略しています。

`Selections` は `Selection` というインターフェースのスライスになっているので、構造体へキャストして `Name` を取り出します。
この例だと、 `formattedBody` が計算を要するFieldなので、これが含まれている場合のみbodyを引き、かつ展開する処理をする `resolveArticleWithBody` を呼び、そうでなければ展開を省略した `resolveArticle` を呼びます。

resolverのメソッドの返り値は構造体である必要があるので `Article` は必要とされるFieldすべての和である必要があります。
そのため `resolveArticle` は常に空の `formattedBody` を返すことになり、ちょっとドキドキしますが、そこはGraphQLのレイヤで参照されないことが担保されます。


その気になればDBから引くカラムをSelectionsに限定することもできそうですが、少なくともGoのORMの表現力だとコストばかりかかって大変そうなので、パフォーマンスに著しい影響がありそうなところだけピンポイントで使うのがよさそうです。