KESSAIでバックエンドのエンジニアをやっている@garsueです。先日、社内向けサービスの新規開発でGraphQLを採用することになりました。今回はその経緯や実装方法についていくつか参考記事を交えながら紹介していきます。GraphQLか今回新規で開発するサービスは以下のような特徴があります。Goにある内容をそのまま適用できそうだなというところからGraphQLを検討し始めました。懸念点としてGraphQL(に限らずグラフデータ構造に対するクエリは一般的に)は複雑で現実的な時間で結果が返ってこないことがままあります。いった事情からいきなりGraphQLを外部に公開するサーバーで実戦投入するのはちょっと怖かったのですが、今回は社内向けでユーザーも限られるため導入の第一弾としても丁度良かったです。一方で社内向けサービスの開発にそこまで工数をかけていられないという事情もあり、スピード感のある開発ができることも重要です。Schemaから大部分のボイラープレートコードを生成してくれます。そのおかげで開発者は基本的に生成されたインターフェイスに基づいてドメインロジックの実装に注力できます。Schemaはクライアント側(ブラウザのフロントエンドやスマートフォンアプリなど)のライブラリでもコード生成に使えるのでサービス全体の開発効率もあがります。APIと似ていますが、GraphQLの場合はSchemaをもとにクライアント側で任意のクエリを記述できる点が異なります。今回はWebのフロントエンドのGraphQLライブラリとしてapolloを採用していたので、フロントエンドではapollo-cliでコード生成することになりました。機能について紹介します。GoでGraphQLサーバーを実装するにあたって、まず検討するのはgraphql.orgでも筆頭で紹介されているgraphql-go/graphqlあたりになるかと思います。graphql-go/graphqlは基本的にSchemaを利用することはなく、コード内でSchemaを記述していくことになります。今回GraphQLを採用した理由がSchemaによるところが大きかったため、graphql-go/graphqlは採用しませんでした。Issueを見る限りやはりニーズはありそうなので、今後何か実装されるかもしれません。Schemaをフルに活用したいとなると、gqlgenが最有力候補に挙がってきます。生成されるコードもリフレクションを使ったり、interface{}を連発して型情報が落ちたりしていないため、読みやすく扱いやすいです。生成されるモデルの代わりにユーザーが定義した構造体をベースにコード生成させることも可能です。この機能が強力で、GraphQLによるアプリケーションサーバーを実装する上で大きな手助けになります。KESSAIでの使い方を紹介していきます。生成の基本パターンgqlgenでコード生成する場合、以下のような生成パターンがあります。KESSAIでの使い方に則して説明していきます。initしたものとします。ID!}Userを引く際に所属するgroups一覧を引いてくる、場合によっては脱退した(left=true)groupsも引いてくるようなユースケースを想定したSchemaです。完全にschemaからモデルを生成させるパターンまずは既存の構造体との紐付けを一切行わず、完全にgqlgenにコード生成させる場合です。この場合、以下のようなモデルが生成されます。`json:"groups"`}愚直にそのままGoの構造体に翻訳された感じですね。Resolverは以下のようになります。error)}こちらも素直な出力結果ですね。アプリケーション側では上記のResolverインターフェイスを実装することで開発を進めていきます。このようなシンプルなResolverはクエリで引いてくるオブジェクトの子フィールドがScalarもしくは親オブジェクトと必ず一緒に取得されるオブジェクトのみで構成される場合で有用です。上記の例で言えば、クライアントから投げられたクエリでgroupが指定されているかどうかによらず、内部的には必ず取得するような場合です。GraphQLサーバーから呼び出すDBや外部サービスが常に子フィールドも含めて返してしまうような場合はこの形式のResolverを使うとシンプルになりそうです。デメリットとしてSchema上で定義されているUserのgroupフィールドに対する脱退フラグの引数leftは直接コード上に現れてきません。リクエストコンテキスト内のResolverContextから指定された引数にアクセスできるみたいですが(未検証)、気軽にアクセスできるとは言えません。その場合は次のパターンを使って定義済みの構造体にマッピングさせることで改善できます。モデルを生成させる代わりに既存の構造体を指定するパターンgqlgenではアプリケーション側で定義済みの構造体をモデルとしてResolverを生成することもできます。ここではSchema上のUserとGroupに対応するものとして以下のような構造体がアプリケーション側で定義されているとします。この構造体をResolverのモデルとして指定するため、以下の設定ファイルgqlgen.ymlを作成します(パッケージ名github.com/garsue/graphql-example/gqlは適宜読み換えてください)。github.com/garsue/graphql-example/gql.Groupこの設定とともにgqlgenを実行するとモデルは生成されず、Resolverのみ生成されます。出来上がるResolverは指定したモデルを利用するものになっています。生成されたコードを見るとアプリケーション側で定義したGroupsメソッドも呼び出されている事がわかります。nil})...(中略)...}これでフィールドに対する引数についても扱えるようになりました。このパターンにも欠点があります。例示したGroupメソッドは、ユーザーがメンバーになったことがあるgroupを全件取得済みであることを前提した実装になっていて現実的ではありません。ざるを得なかったのはUser取得時点で、Userに対するあらゆるクエリに対応する必要があったためです。Groupフィールドが指定されていないクエリだったとしても常にgroupを引かなければならず、GraphQLの利点を活かせていません。柔軟にフィールドの取得を制御するためには、Groupを取得するResolverを生成させる次のパターンが有効です。子フィールドを取得するResolverを生成させるパターン最後に、子フィールドを取得するResolverも生成させるパターンを紹介します。このパターンが最も強力でgqlgenのよくできているところです。子フィールドを取得するResolverを生成させるには、アプリケーション側で親オブジェクトのモデルと子フィールドのモデルを直接紐付けなければいいだけです。以下のような構造体がアプリケーション側で定義されているとします。string}UserにGroupsフィールドもGropusメソッドもなく、この定義だけではUserとGroupを紐付ける手がかりがありません。この状態で先程と同様に生成すると、以下のResolverインターフェイスが生成されます。error)}新たにUserResolverというResolverが生成されました。UserResolverはUserをもとに、Groupを引いてくるGroupsメソッドを持っています。フィールド引数のleftも渡ってきていますね。このGroupsメソッドがUser取得時のクエリに応じて呼び出されます。あとはUserResolverを実装として、その中で親オブジェクトやフィールド引数の条件にあったGroupリストを取得するコードを記述するだけです。このGroupsメソッドはクエリでgroupsが指定された場合のみ呼び出されるため、不必要にDBや外部サービスへ問い合わせることもありません。基本的に子フィールドとして何らかのドメインオブジェクトを含む集約的なオブジェクトに対してはこのパターンのResolverを生成させることになるかと思います。逆にScalarや値オブジェクト(座標、日付など)のみで構成されるオブジェクトの場合は自動生成させてしまってもいいかもしれません。KESSAIにおけるgqlgenの使い方を紹介します。基本方針としてGraphQLサーバーを薄く保ちたいため、なるべく手でロジックを書くことを避けて、可能な限りgqlgenのコード生成に寄せています。その中で他のオブジェクトをまとめる集約的なオブジェクトに対しては専用のResolverを生成させています。Resolverの実装も不要なロジックが入り込まないようになるべく薄く、条件に応じたデータを取って返すだけになっています。何らかのドメインロジックが必要になった場合でも、そもそもそのような状況になっている事自体が間違いだと捉え、クライアント側で解決したりGraphQLサーバー依存する外部サービス側で解決したりしています。課題GraphQLサーバーを実装するにあたってのよくある課題についても紹介します。クライアントはクエリをいくらでも複雑にできる「クライアントはクエリをいくらでも複雑にできる」とは、例えば「ユーザーが所属するグループを取得、そのグループに所属する他のユーザーを取得、そのユーザーが所属する別のグループを取得、そのグループに(以下略)」のような循環するクエリを書けてしまうことです。この問題に対する対応としてはクエリ内のネスト回数を制限するなどになるかと思います。gqlgenであればmiddlewareとしてネスト回数の制限機構を記述できるような気もするのですが、まだまだ手が回っていないところです。n+1問題GraphQLではフィールド単位でデータの取得が行われるのでn+1問題が容易に発生します。gqlgenではその対応としてdataloadenの利用を推奨しています。やりたいこととしてはdataloaderと同じで、データ取得結果をキャッシュして外部への問い合わせを極力減らそうというものです。こちらもまだまだ検証できておらず未評価です。こちらについてはまた別途ご紹介できればと思います。KESSAIでのGraphQLの導入状況については書いたとおり、まだまだ模索段階です。今後また新たな知見が得られたらこちらのブログにて紹介したいと思います。GraphQLで紹介されているのでご興味のある方は聞いてみることおすすめします。[return]この分類は筆者が勝手に定義したものです。