何をするか?
Golang の GraphQL ライブラリである gqlgen
を使って、GraphQL スキーマからサーバー実装用のコードを自動生成するときに、オブジェクト型の子フィールド用のリゾルバーを生成する方法を説明します。
gqlgen
の基本的な使い方は下記を参照してください。
デフォルト設定でコード生成した場合
ここでは、入力用の GraphQL スキーマとして次のようなファイルを使うことにします。
オブジェクト型として Book
と Author
があり、Author
は Book
の author
フィールドとしてのみ使用されています。
gqlgen generate
すると、次のようなモデル(型情報)コードが生成されます。
さらに、リゾルバーのテンプレートコードとして次のようなメソッドが自動生成されるのですが、デフォルトの設定 (gqlgen.yml
) では、Query
型のフィールドに対応するリゾルバーメソッドしか生成されません。
例えば、今回のスキーマの場合 books
フィールドを取得するためのリゾルバーメソッドのみが生成されます。
ちなみに、上記メソッドシグネチャ内の、queryResolver
と Books
という名前は、「Query
オブジェクト型の books
フィールドを取得するためのリゾルバー」であることを示しています。
Book
オブジェクト型の author
フィールドを取得するためのリゾルバーは生成されないので、books
フィールド用のリゾルバー実装だけで、すべての子フィールドのデータを返すように実装しなければいけません。
例えば、2 つの書籍データを返すリゾルバー実装は次のような感じになります(強引にハードコードしてます)。
func (r *queryResolver) Books(ctx context.Context) ([]*model.Book, error) {
// フェイクデータ(本当は別パッケージ化して db.GetBooks() のようにすべき)
books := []*model.Book{
{
ID: "book-id-1",
Title: "Book title 1",
Author: &model.Author{ID: "author-id-1", Name: "Author name 1"},
},
{
ID: "book-id-2",
Title: "Book title 2",
Author: &model.Author{ID: "author-id-2", Name: "Author name 2"},
},
}
return books, nil
}
オブジェクト型のフィールドがこれくらいシンプルであれば何とかなるのですが、入れ子構造が深くなってくると、子孫フィールドをすべて処理しなければならず大変です。
また、上記の例では、クライアントから author
フィールドが要求されているかどうかにかかわらず著者情報を DB からフェッチしており(今回はハードコードですが)、それも無駄です。
さらに、フィールドに検索やフィルタ用の引数が追加されたら、その引数に応じたデータフェッチ処理を行わなければならず、ますます複雑になってきます。
クライアントから実際に指定されたフィールド引数の値は、リゾルバーのパラメーターとして渡される context.Context
オブジェクトを使って graphql.GetFieldContext(ctx)
のようにすれば参照できるのですが、これはこれで大変です。
author
フィールド用のリゾルバーを、別メソッドとして定義できれば、実装がぐっと楽になります。
フィールド用のリゾルバーを生成する
やりたいことは、GraphQL スキーマで定義した Book
オブジェクト型の author
フィールドの専用リゾルバーを生成する、ということです。
そのためには、gqlgen
の設定ファイル (gqlgen.yml
) の models
プロパティで次のように設定します。
「Book
型のフィールドである author
のリゾルバーを生成する」という設定です。
そのまんまですね。
このように設定した状態で、gqlgen generate
を実行すると、リゾルバーのテンプレートとして次のようなコードが生成されるようになります。
Book
オブジェクト型の author
フィールドを処理するための専用のリゾルバーメソッド (*bookResolver) Author
が生成されたので、books
リゾルバー側の実装では author
フィールドを処理する必要がなくなります。
下記はフェイクデータを使用したリゾルバー実装例です。
// Author is the resolver for the author field.
func (r *bookResolver) Author(ctx context.Context, obj *model.Book) (*model.Author, error) {
// author フィールド用のフェイクデータ(親オブジェクト型 Book の情報を利用して実装できる)
author := &model.Author{
ID: "author-id-" + obj.ID,
Name: "Author name of " + obj.Title,
}
return author, nil
}
// Books is the resolver for the books field.
func (r *queryResolver) Books(ctx context.Context) ([]*model.Book, error) {
// books フィールド用のフェイクデータ(ここで author フィールドのデータは返さなくてよい)
books := []*model.Book{
{ID: "book-id-1", Title: "Book title 1"},
{ID: "book-id-2", Title: "Book title 2"},
}
return books, nil
}
前述の (*queryResolver) Books
リゾルバーのみを使った実装よりも、だいぶ分かりやすくなったと思います。
もちろん、クライアントから author
フィールドを要求されなかった場合は、(*bookResolver) Author
リゾルバーが呼び出されることはないので、余計な DB フェッチが行われる心配もありません。
バッチシですね!
ちなみに、フィールド引数を扱うときもほぼ同様の実装で対応できます。
例えば、GraphQL スキーマの Book
オブジェクト型の author
フィールドに、次のような realName
パラメータを追加したとします(本名を取得するためのフラグのつもり)。
すると、author
フィールド用のリゾルバーメソッドのパラメーターにも realName
が追加されるので、その情報を使ってリゾルバーを実装することができます。
言語仕様上、Golang 関数の realName
パラメーターではデフォルト値が表現できていませんが、クライアントからのクエリで realName
引数が省略された場合は、ちゃんと GraphQL スキーマで定義されたデフォルト値(今回は false
) を受け取れるようになっています(これは gqlgen
フレームワーク側の仕組みです)。
完璧ですね!