💻

【GraphQL × Go】gqlgenの基本構成とオーバーフェッチを防ぐmodel resolverの実装

こんにちは。SaaS事業でLayerX ワークフローの開発を担当している@sh_komineです。 この記事は、LayerX Advent Calender 2021の16日目の記事です。
LayerX ワークフローではGoとGraphQLをフル活用して開発を行なっています。
GraphQLの良さはいろいろと語られていますが、「Goで実際にどう実装するんだ?」と言うところは、gqlgenの簡潔なGet Startedがあるくらいでなかなか手で動かさないと理解できないなという思いがありましたので、graphqlのプロジェクトの基本構成に触れながら、オーバーフェッチを防ぐ実装の仕方について書いていきたいと思います。
本記事は実際の事例ではなく、gqlgen 初学者の全体把握、gqlgenの仕組みについての理解に焦点を当てた記事になります。
以下の流れで話をします。
  1. graphqlの基本構成
  1. オーバーフェッチングを防ぐためのmodel resolverの実装
  1. 補足  の分割

今日はしない話

フロントエンドと組み合わせた全体の話や、DBなどの処理も含めた全体を通した話はしません。 実装例については、尊敬する同僚の@mosaさんや@anagoさんが過去に書いた記事がありますので、そちらをご覧ください。
  • GraphQLを用いたフロントエンドからバックエンドまで開発の流れを紹介した記事

1. graphqlの基本構成

graphqlの基本構成について理解するために、以下の流れで見ていきます。 運用しているとどんどんファイルは大きくなり、全体像の把握が難しくなってくるので、一番最初のシンプルな構成から内容を見ていきます。
  • 1-1. スケルトンプロジェクトを作る
  • 1-2. 生成されたファイルのそれぞれの役割について

1-1. スケルトンプロジェクトを作る

Getting Startedに倣い、Goのプロジェクト内でgqlgenをインストールして、スケルトンプロジェクト(雛形)を作成します。 (多分、どのプロジェクトでも導入の際はここから始まると思います。)
スケルトンプロジェクトは以下のように作成されます。
めちゃくちゃ簡単に雛形ができました。これがgqlgenのシンプルな基本構成になります。 rootの構成としては、以下のようになっています。
  • gqlgen.yml: 設定ファイル
  • graph: gqlgenの開発で利用するファイルがいろいろと含まれるパッケージ(ここのファイルについては後述)
  • server.go: main関数
次は生成されたファイルのそれぞれの役割について見ていきます。

1-2. 生成されたファイルのそれぞれの役割について

 : GraphQLのスキーマ定義ファイル

 はGraphQLのスキーマ定義ファイルで、APIエンドポイントとその型を管理するのが役割です。 スケルトンプロジェクトでは以下のように生成されます。 以後、自分が書き加えた箇所には ⭐️ をつけていきます。
gqlgenでは基本的にこの というGraphQLの定義ファイルを編集し、
というコマンドを実行することで、後述する  や  を再生成して開発を進めていくので、全ての起点になるファイルです。 gqlgenの話の前にGraphQLの型定義のより詳しい仕様が知りたい方は公式のドキュメントをご覧ください。

 :  から自動生成されたGoの型定義ファイル

 は  から自動生成されたGoの型定義ファイルです。 上記の  と見比べると分かりますが、  と  以外の  と  のstructが自動生成されています。
 と  は  には作成されません。  に作成されます。
また詳しくは後述しますが、 配下には自分で定義したGoのモデル定義のファイル  を置くことができます。 自分で定義したGoのモデルも gqlgen コマンドで読み込まれ、をベースにに足りないFieldが  に生成されます。 ひとまずは、 パッケージはGraphQLの型オブジェクトに対応するgo のstructを管理する役割があると覚えてください。

 :  ファイルから自動生成されたエンドポイント実装用のGoファイル

 は  ファイルから自動生成されたエンドポイント実装用のGoファイルです。エンドポイントの管理が役割になります。 最初の状態は以下のように未実装の状態でコードが生成されているので、そこに実装をしていく流れになります。
resolverはよくあるcontrollerやhandlerと役割は同じなので、そこまで難しく考えなくて大丈夫です。 この中の詳細の実装はresolverに処理をベタ書きでも、MVCでもDDDでも実装者の好きに実装していくことができます。
ここで、上記の  や のコードを見てみると
どちらも  の型定義を含んでおり、状態を持つ役割はResolverに集約されています。

 :  コマンドで再生成されないgoパッケージ

 の型定義は  に定義されていて、状態を集約して管理するのが役割です。 自動生成される  から参照されているので、型の名前を変えたり消すことはできませんが、この  は初期生成の後コマンドを実行しても再生成されることはないので、このstructに参照し続けたいインスタンス(pointer)などを持たせることができます。 MVCなら各model、その他 repositoryやservice, usecaseのstructを持つような実装になるかと思います。
 インスタンスを作成しているのは  です。 Goのmain関数が含まれています。 このファイルも最初にスケルトンオブジェクトで作成された後はコマンドの実行で再生成されないので、自由に書き換えることができます。
ここまで初期生成されたスケルトンプロジェクトを元にgqlgenの基本構成を説明してきました。 実際に動かしてみるところまでやってみたい方は、さくっと終わるので公式のGetting Startedをやってみてください。 より深く理解ができると思います。
次はいよいよオーバーフェッチングを防ぐためのmodel resolverの実装について触れていきます。

2. オーバーフェッチングを防ぐためのmodel resolverの実装

2-1. オーバーフェッチとは

オーバーフェッチ = リクエスト元で必要ないのに余分にリソースをフェッチしてしまうこと
です。
gqlgenのREADMEのよくある質問の最初にも「使わないかもしれない子オブジェクトのフェッチを防ぐにはどうしたらいいか? 」という項目が用意されており、GraphQLの重要なテーマであることがわかります。 GraphQLでは通信元のqueryで取得するリソースを制御することができ、より柔軟で効率的な情報取得が可能です。
例えば、上記のGet Startedの例では、Todoの中にUserが含まれていますが、これは、必ず必要とは限りません。
GraphQLでは情報の取得側がqueryで明示的に指定することによりそのデータが必要かどうかサーバー側に伝えることができます。
userの名前が必要であれば、以下のようにリクエストをします。
結果
また、userが必要なければ、以下のようにuserを含めずにリクエストをします。
結果
gqlgenではこのuserがリクエストに含まれた時だけ呼ばれるメソッドをに作成することができます。 これによって、無駄なDBフェッチが走らない実装が可能になります。 gqlgenのドキュメントでは、特に名前がつけられていませんが、の中に 以外の モデルごとのResolver  が生成されるので、ここでは勝手にmodelresolverと呼ばせていただきます。(上記の例では が生成されます。)

2-2. model resolverの2つの実装方法

gqlgenのREADMEにも書いてありますが、model resolverの実装には2つの方法があります。
  • カスタムモデルを用いた暗黙的な生成
  • gqlgen.ymlへの記載による明示的な生成
Get Startedでは「カスタムモデルを用いた暗黙的な生成」しか触れられていませんでしたし、自分が試した感触でもそちらの方が便利だったので、カスタムモデルを用いた実装方法を強くおすすめします。

カスタムモデルを用いた暗黙的な生成

model packageの説明で少し触れましたが、gqlgenは での定義と  内の型定義の差分によって  を生成します。
今、 でのTodoの定義は以下の通りです。
 内のTodoを削除し、というファイルにカスタムモデルを定義します。 その際、 内のUserを丸っと消してみます。
つづいて、 コマンドを実行すると、 の型定義をベースにのカスタムモデルで足りないfieldを検知してにmodel resolverを追加します。
ただ、上記で生成された状態だとTodoのIDでUserの情報を取得しないといけません。 実際の場合はTodoがUserIDを持っていることの方が多いと思います。 そこで、にUserIDを足します。
これでmodel resolverの中でUserIDを利用できるようになりました。
にUserIDを追加しても には変更を入れていないので、GraphQLのインタフェースには変化がありません。 このように外には出したくないが、relationを追加するために必要なパラメータなどを自由に定義できるので、カスタムモデルの実装はおすすめです。

gqlgen.ymlへの記載による明示的な生成

自分もあまり使っていませんが、一応もう一つのmodel resolverの生成方法もご紹介します。 以下のように、  に具体的な指定をしても、カスタムモデルの時と同様にresolverを生成することができます。
生成できました。 ですが、こちらの方法だとTodoのモデルにUserIDなどのrelationを追加するために必要なパラメータをgoのモデルに追加することができません。 また、Inline config with directives という機能でより詳細に設定できるそうですが、自分が手元で試した際にはうまく動かなかったので、「カスタムモデルを用いた暗黙的な生成」の方を利用することをおすすめします。

2-3. 具体的なmodel resolverの使用例

ここまで model resolverの実装方法について話してきましたが、重要なのは の型定義をベースにのカスタムモデルで足りないfieldを検知してにmodel resolverを追加します。という部分です。 つまりオーバーフェッチがある部分に関しては、カスタムモデルから削除するだけでqueryで指定されなければ呼び出されないmodel resolverを生成することができます。
例えば以下のような場合に便利です。

Todoにタグ情報が複数紐づく場合にmodel resolverを活用

複数のRelationがあるケースです。
一点注意点としては、 todoResolverのメソッドは、以下のようにTodoを配列で取得する場合、返却するtodoの件数分呼び出されます。 つまり、有名なN+1問題が発生します。
このN+1問題の解消には、dataloaderという仕組みがあり、実際にはそれで実装しているのですが、 話し始めるととても長くなるのでまたの機会にさせていただきます。 ざっくり言うと、データの呼び出しを一定期間待ち合わせた後に一斉にデータフェッチしてくる仕組みです。 気になる方は 「golang graphql dataloader」で調べてみてください。

provileURLを求められない限りは S3 アクセスしないためにmodel resolverを活用

単一のフィールドであってもCloudのインフラにアクセスする必要があるケースです。 relationだけではなく、こういった単一のフィールドであってもオーバーフェッチを避ける事ができます。

3. 補足 graph/schema.graphqls の分割

余談ですが、実プロジェクトを回していると、はどんどん大きく膨れていってしまい、メンテナンスが大変になります。 この  は簡単に分割できて、例えば、
という分割の仕方をすると、
というようにresolversファイルが分割され、全体の見通しをよくすることができます。 この際、に関しては分割されません。 ファイルがとても大きくなってきた際には是非試してみてください。

最後に

今日はgqlgenの基本構成とオーバーフェッチを防ぐmodel reolverの実装について書いてみました。 gqlgenの基本的な仕組みの理解の助けになったら嬉しいです。

We are Hiring

LayerXではエンジニアはもちろん、全職種で積極的に採用中です。 ぜひカジュアル面談からでもお話しましょう!
  • LayerXのEntranceBook
LayerXの事業やチームについて、もっと知りたい方はこちら
  • LayerXのMeety一覧
LayerXのメンバーと、カジュアルに話してみたい方はこちら
  • LayerXの募集ポジション一覧
実際に応募してみたい方はこちら