gqlgen とは
gqlgen は go 言語製 GraphQL ライブラリで、自分で定義した GraphQL スキーマを元にリゾルバーのインターフェースを生成してくれて、あとはリゾルバーの実装をすれば、簡単に GraphQL のスキーマファースト開発ができるという物です。
バインド機能
gqlgen にはデータモデルのバインド機能があり、GraphQL の型とアプリケーションの Go の構造体を紐づけて自動でフィールド解決をしてくれるようにすることができます。
# entity.gql
type User {
id: ID!
name: String!
tags: [Tag!]!
}
// entity/user.go
type User struct {
ID int
Name string
}
// entity/tag.go
type Tag struct {
ID int
text string
}
# gqlgen.yml
models:
User:
model: github.com/yourpkg/entity.User
Tag:
model: github.com/yourpkg/entity.Tag
# または
autobind:
- github.com/yourpkg/entity
上記のようにすると User の解決時に ID と Name は自動で User 構造体のフィールドをそのまま利用し、 構造体に存在しない tags だけ以下のようにリゾルバーを書くことで解決することができます。
func (r *userResolver) Tags(ctx context.Context, obj *User) ([]*Tag, error) {
return r.tagUsecase.GetTagsByUserID(ctx, obj.ID)
}
または、構造体を以下のようにすることで、自分でリゾルバーを書かない方法もあります。
// entities/user.go
type User struct {
ID int
Name string
Tags []*Tag
}
このようにして、User の取得時に eager loading することで、全てのフィールドを自動で解決してもらうことも可能です。
構造体の自動生成機能
gqlgen には Go の構造体定義を GraphQL のスキーマから自動で生成してくれる機能もあります。バインド機能を利用せずにこの機能を使うと、先ほどのスキーマから以下のような構造体を作ってくれます。
type User struct {
ID ID `json:"id"`
Name string `json:"name"`
Tags []*Tag `json:"tags"`
}
どのように使い分けるか
これから新しく gqlgen で GraphQL API を作る場合、「自作構造体へのバインド」or 「リゾルバーの実装」or「スキーマから自動生成したモデル」をどのように使い分けるかのヒントを紹介します。
基本的考え方
いきなりですが、自分は基本的に全てエンティティーに対して自動バインドを有効にしています。
そもそも gqlgen の良さは GralphQL のスキーマを先に決めて、それに答えるバックエンドを作る「スキーマファースト開発」ができる点だと考えています。スキーマからリゾルバーの作成を gqlgen に任せ、リゾルバーがアプリケーションのユースケースを呼び出すことで、アプリケーション内部に GraphQL という概念が入り込まないようにしています。なので、GraphQL のスキーマとは全く別に、アプリケーション用のエンティティーを定義しています。その二つがたまたま一致する部分を自動バインドで解決してもらい、そうでないフィールドのリゾルバーを個別に実装するという考え方です。
フィールドのリゾルバーを実装
構造体にバインドして解決させるということは、その構造体がフィールドを満たした状態で存在しなければならず、RDBMS をデータストアにしている場合、上記の Tags などはリポジトリ層で取得する際に必ず JOIN させて eager loading してなければなりません。これは n+1 を回避できるメリットもありますが、必要なフィールドだけリクエストできるという GraphQL がクエリ言語であるという特性を無視していることになります。
そのため、アプリケーション内でも必ず必要となるような最低限の物だけは JOIN させてリポジトリ層がユースケース層に返す時点でフィールドに存在することを保証し、それ以外の物は基本的にリゾルバーに一任するようにしています。リゾルバーが並列で走るため生じる n+1 を解決したい場合は別途データローダーなどを活用して回避しています。
(実際にはリポジトリ層はデータベースのテーブルと対になるテーブルモデルを利用し、エンティティーモデルへの詰替をすることで、gorm 用のタグやメソッドなど DB に関するあれこれがユースケース層に漏れないようにしています)
スキーマからの構造体自動生成の活用
上記の考えより、スキーマからの自動生成は主な型には基本的には使っていません。しかし、ユースケース層に入り込まず、リゾルバーの層にのみ必要な型のみ、自動で生成しています。
mutation に必要な input 型やページネーション用の型などは GraphQL 専用の型なので、自動生成したものをリゾルバーでのみそのまま利用しています。
(他にも Fragment 用の interface 定義を自動生成に任せ、そのエンティティーがダックタイピング的に該当の Fragment であることを示したりもしていますがここでは割愛。)
まとめ
- GraphQL スキーマとアプリケーションの型は根本的には別に考える
- その上で一致したものは gqlgen に任せる
- 別で考えてもスキーマが要求するものを返すアプリケーションを作るわけだから結局似る
- それ以外はリゾルバーで解決する
- n+1 を無理に eager loading で解決しようとしない
- リポジトリ層はシンプルに
- 構造体の自動生成は input などの表層モデルでのみ利用する
- ビジネスロジックが使うモデルを GraphQL や gqlgen に依存させない
以上です。