基本編」にて実装を行った際には、GraphQLのスキーマで定義されたクエリ・ミューテーション1つに対してリゾルバメソッド1つが紐づいている状態でした。*mutationResolver型のAddProjectV2ItemByIDsメソッドを実行この章ではリゾルバを分割することによって、この1:1対応を解消していきます。リゾルバを分割する前の状況確認まずは、リゾルバ分割を行っていない状況ではどのような挙動をしているのか、もう一度確認してみましょう。応用編では、repositoryクエリを例にとって説明していきたいと思います。repositoryクエリと得られるレスポンス型repositoryクエリは、レスポンスとしてRepositoryオブジェクトを返すように定義されています。Repository}そのRepositoryオブジェクトの中には、スカラ型のフィールドが3つ、非スカラ型(オブジェクト型)のフィールドが5つずつ存在しています。PullRequestConnection!}リゾルバ分割前に得られるレスポンスrepositoryクエリを実行して、得られるレポジトリ情報を全て表示させてみようと思います。そのようなクエリは以下のような形になります。}}しかし、基礎編の内容に従って実装していくと、以下のような少しおかしなレスポンスが得られるかと思います。}}おかしなポイントは以下2つです。取得したレポジトリのオーナー名はhsakiであるはずなのに、レスポンスではowner.nameフィールドが空文字列になっており取得できていないオブジェクト型に対応したフィールド(issue(s)・pullRequest(s))がnullになっておりデータ取得できていないrepositoryクエリに1:1対応づけされたリゾルバメソッドの実装どうしてこのようなクエリ実行結果になってしまうのか、原因を確認します。現在、repositoryクエリを実行した際に呼び出されるリゾルバは一つだけです。name)}レスポンスを作る際のキーとなる部分は、サービス層のGetRepoByFullNameメソッドを実行している部分です。このGetRepoByFullNameメソッドの中で取得しているのは、DBに用意されたrepositoriesテーブル中のデータのみにしています。repo.CreatedAt,}}本来ならばテーブルのjoinなどを行って、レポジトリに紐づいたIssueやPRの情報を取得するべきなのですがそれを行っていないため、DBのrepositoriesテーブル内にある情報しかレスポンスに含めることができないのです。N/A!現時点でGraphQLのどのクエリフィールドに紐づくことがない、repositoryテーブルのowner列をわざわざselectしてくる意味はあるの?と思う方もいるでしょうが、これが後々重要になってきます。リゾルバ分割の実装リゾルバを分割していない今の状況ではどのような不具合があるのかを確認できたところで、いよいよ分割実装をしていきましょう。gqlgen.ymlに分割設定を記述リゾルバを分割する設定は、gqlgen.ymlに記述します。true今回の分割の方針は「repositoryクエリを実行して得られたRepositoryオブジェクトの中で、おかしなことになっていたフィールドを切り出す」というもので、models.Repository.fields直下に今回の対象としたいフィールド名(owner/issue(s)/pullRequest(s))を列挙しています。分割したリゾルバコードの生成gqlgen.ymlにリゾルバ分割の設定を記述したら、その内容にしたがってコードを再生成させます。generateすると、graph/schema.resolvers.goの中に以下のコードが増えていることが確認できるかと思います。分割された子リゾルバ部分のコードは以下の通りです。Issue......などができています。次はこの新規生成されたメソッドの中身を実装していくことになります。Issueメソッドメソッド内の処理まずはIssueメソッドの中身を実装していきましょう。issue"))}このメソッドは以下のように、repositoryクエリを使って取得するRepositoryオブジェクトのissueフィールドにアクセスされたされたときに呼び出されるものです。}}そのため、メソッドの中に実装するべき処理は「とあるレポジトリに属する、とある番号のIssue情報をDBから探してきて返り値にする」というものになります。DBのissueテーブルにアクセスするサービス層メソッドを作成し、それをリゾルバの中から呼び出すようにしてあげましょう。issue.Repository},}}メソッドの引数として与えられている*model.Repository型について特筆するべき点としては、このIssueメソッドの引数として*model.Repository型が与えられており、その中には取得対象となったレポジトリの情報(IDやcreatedAtなど)が含まれています。そのため、Issueメソッドの中でobj.IDを参照することで「検索対象となったレポジトリのID」を入手することができるのです。リゾルバの呼び出し順どうしてIssueメソッドの*model.Repository型引数にあらかじめレポジトリの情報が格納されていたのか、それは分割されたリゾルバの実行順が関わっています。今回のように「repositoryクエリを使って取得するRepositoryオブジェクトのissueフィールドにアクセスする」場合のクエリをよく観察してみます。}}すると、以下のような構造になっていることがお分かりいただけるかと思います。queryというワードによって、クエリ・ミューテーションと数あるGraphQLの操作の中でクエリを行いたいということが確定するrepositoryというワードによって、クエリの中でもrepositoryクエリを実行したいということが確定するissueというワードによって、Repositoryオブジェクトの中でのissueフィールドが欲しいということが確定するこれは、そのままリゾルバを呼び出す順番にもなっているのです。(ユーザー実装部分、略)}そのため、リゾルバ*queryResolver型のRepositoryメソッドが呼ばれて、その過程でrepositoryテーブルから取得対象のレポジトリの情報を取得→*model.Repository型に格納1で得た情報を引数にして、リゾルバ*repositoryResolver型のIssueメソッドを呼ぶという処理フローを作り上げることができるのです。OwnerメソッドIssueメソッドと同様の考え方で、Ownerメソッドも作っていきましょう。owner"))}メソッド内の処理Ownerメソッド内で実装するべき内容は「とあるレポジトリのオーナーとなっているユーザー情報を取得する」というものです。オーナーとなっているユーザーIDは第二引数のobj.RepositoryのOwner.IDフィールドに格納されているため、それを利用してusersテーブル内をselectすればOKです。nil+}レポジトリオーナーのユーザーIDが手に入った理由さて、Ownerメソッドの中では、引数として与えられたobj.Repository型のOwner.IDフィールドを参照することでレポジトリオーナーのユーザーIDを得ることができました。実はこれは、repositoryResolver.Ownerメソッドが呼ばれる前に実行されたqueryResolver.Repositoryメソッド、いわば親となるリゾルバの中できちんとそのような実装をしたことがキーになっています。Issueメソッドの実装の際にも解説した通り、分割されたリゾルバは以下のようにネストが浅い順に呼ばれていきます。ルートリゾルバ*Resolver型のQueryメソッドが呼ばれるリゾルバ*queryResolver型のRepositoryメソッドが呼ばれるリゾルバ*repositoryResolver型のOwnerメソッドが呼ばれるステップ2のRepositoryメソッドで作成し、戻り値としている*model.Repository型が、そのまま後続ステップ3のOwnerメソッドの引数となります。リゾルバの戻り値というのは、単純にクライアントに返却するレスポンスを作るという以外にも、後続の子リゾルバに渡す引数を作っているという役割・側面があるのです。error)Repositoryメソッドの戻り値を作っているGetRepoByFullNameサービスでは、repositoryテーブルの4つの列をselectしてきていましたが、その場でユーザーレスポンスという形で生きたのはそのうちの3つだけでした。その場では何の役割もなかったowner列の情報は、後続のリゾルバOwnerメソッドの中で「レポジトリオーナーのユーザーIDを入手する」という機能をしっかりと提供するのです。N/A(オーナーとなるユーザーIDはrepositoriesテーブルのowner列から取れているが、それだけでは不足している。repo.CreatedAt,}}このように、サービス層の中でテーブルデータをselectしてくるときは「テーブルjoinが必要にならない範囲で、できるだけ多くのデータを取得してモデル構造体に反映させる」ことで、後々リゾルバを分割したときに役に立つのです。今回Ownerメソッドを実装するにあたり「ユーザーIDから、userテーブル内のユーザーデータを取得する」という処理が必要になったため、それをサービス層のGetUserByIDメソッドとして実装しました。先読みした話をすると、例えば今後ProjectV2オブジェクト関連のリゾルバを分割していく際に、同様の処理が必要になります。obj.Owner.ID)}このとき、サービス層という形で処理を分離して実装したことによって、異なるリゾルバ間で同様の処理を使い回して楽をすることができるようになっていることに気づくかと思います。基本編にて「なぜリゾルバメソッドの中に直接DBクエリ処理を書かず、わざわざサービス層に切り出したのだろう?」と思った方もいるかもしれませんが、リゾルバというのは適切に分割していくとどうしても似たような処理を複数箇所に記述するということになってしまいます。そのため、ビジネスロジック自体は他のパッケージに切り出して、リゾルバからはそれらを呼び出すだけ、という形にすることでコードをスッキリさせることができます。動作確認ここまで実装できたところで、分割したリゾルバを実際に稼働させてみましょう。!この記事ではOwnerメソッドとIssueメソッドしか実装していませんが、余裕のある方は他のメソッドの実装・リゾルバの分割もしてみてください。サーバー稼働サーバーを稼働させるために、エントリポイントであるserver.goを実行します。playgroundリクエストクエリの記述サーバーを稼働させたら、リクエストクエリを作ります。今回は、新たに実装したOwnerメソッドとIssueメソッドが呼ばれるようにフィールドを選択してみました。}}リゾルバ分割前には得られなかったowner.nameフィールドとissueフィールドがnullにならず、きちんと取得できていることが確認できました。リゾルバ分割の利点まとめリゾルバを分割したことで得られた利点を改めてまとめたいと思います。オーバーフェッチを防ぐ発行されるSQLクエリを簡潔に保つオーバーフェッチを防ぐRepositoryオブジェクトを取得するためのリゾルバを分割したことによって、ownerフィールドを取得するクエリを受け取ったときにはOwner小リゾルバを呼び、そうでないときは呼ばないissueフィールドを取得するクエリを受け取ったときにはIssue小リゾルバを呼び、そうでないときは呼ばないpullRequestフィールドを取得するクエリを受け取ったときにはPullRequest小リゾルバを呼び、そうでないときは呼ばない(以下略)といった処理フローを作ることができました。これによりGraphQLの利点である「欲しいフィールドのみを指定してデータ取得する」という機能を真に実装できたことになります。trueクエリを簡潔に保つリゾルバ分割によって「あるフィールドが呼ばれたときには、別のリゾルバを呼ぶ」仕組みを作り上げたことで、DBからデータを取得するためのSQLクエリをシンプルに保つことができるようになります。複数のテーブルに跨るようなデータ取得を要求されたときに、1つのリゾルバの中でJOINを駆使して何とか一回の処理でレスポンスに必要なデータを読み出す必要はもうなく、別のリゾルバに処理を委譲すればよいのです。次章予告リゾルバを分割したことによって、「リクエストされたデータだけ読み出す・処理する」というGraphQLの肝となる部分をついに実現させることができました。次章は、このリゾルバ分割をした副作用として生まれてしまった「N+1問題」とその解決方法をご紹介したいと思います。