こんにちは。
Part 6は「リゾルバの実装 - 応用編」についてです。
この章について
「リゾルバの実装 - 基本編」にて実装を行った際には、GraphQLのスキーマで定義されたクエリ・ミューテーション1つに対してリゾルバメソッド1つが紐づいている状態でした。
-
user
クエリ:*queryResolver型
のUser
メソッドを実行 -
repository
クエリ:*queryResolver
型のRepository
メソッドを実行 -
node
クエリ:*queryResolver
型のNodeメソッドを実行 -
addProjectV2ItemById
ミューテーション:*mutationResolver
型のAddProjectV2ItemByIDsメソッドを実行
この章ではリゾルバを分割することによって、この1:1対応を解消していきます。
リゾルバを分割する前の状況確認
まずは、リゾルバ分割を行っていない状況ではどのような挙動をしているのか、もう一度確認してみましょう。
応用編では、repository
クエリを例にとって説明していきたいと思います。
repository
クエリと得られるレスポンス型
repository
クエリは、レスポンスとしてRepository
オブジェクトを返すように定義されています。
type Query {
repository(
name: String!
owner: String!
): Repository
}
そのRepository
オブジェクトの中には、スカラ型のフィールドが3つ、非スカラ型(オブジェクト型)のフィールドが5つずつ存在しています。
- スカラ型
-
id
:ID
型 -
name
:string
型 -
createdAt
:DateTime
型
-
- オブジェクト型
-
owner
:User
オブジェクト -
issue
:Issue
オブジェクト -
issues
:IssueConnection
オブジェクト -
pullRequest
:PullRequest
オブジェクト -
pullRequests
:PullRequestConnection
オブジェクト
-
(再掲)GraphQLスキーマに定義されたRepositoryオブジェクト
type Repository implements Node {
id: ID!
owner: User!
name: String!
createdAt: DateTime!
issue(
number: Int!
): Issue
issues(
after: String
before: String
first: Int
last: Int
): IssueConnection!
pullRequest(
number: Int!
): PullRequest
pullRequests(
after: String
baseRefName: String
before: String
first: Int
headRefName: String
last: Int
): PullRequestConnection!
}
リゾルバ分割前に得られるレスポンス
repository
クエリを実行して、得られるレポジトリ情報を全て表示させてみようと思います。
そのようなクエリは以下のような形になります。
query {
repository(name: "repo1", owner: "hsaki"){
id
name
createdAt
owner {
name
}
issue(number:1) {
url
}
issues(first: 2) {
nodes{
title
}
}
pullRequest(number:1) {
baseRefName
closed
headRefName
}
pullRequests(last:2) {
nodes{
url
number
}
}
}
}
しかし、基礎編の内容に従って実装していくと、以下のような少しおかしなレスポンスが得られるかと思います。
{
"data": {
"repository": {
"id": "REPO_1",
"name": "repo1",
"createdAt": "2023-01-09T22:11:47Z",
"owner": {
"name": "",
},
"issue": null,
"issues": null,
"pullRequest": null,
"pullRequests": null
}
}
}
おかしなポイントは以下2つです。
- 取得したレポジトリのオーナー名は
hsaki
であるはずなのに、レスポンスではowner.name
フィールドが空文字列になっており取得できていない - オブジェクト型に対応したフィールド(
issue(s)
・pullRequest(s))
がnull
になっておりデータ取得できていない
repositoryクエリに1:1対応づけされたリゾルバメソッドの実装
どうしてこのようなクエリ実行結果になってしまうのか、原因を確認します。
現在、repository
クエリを実行した際に呼び出されるリゾルバは一つだけです。
// Repository is the resolver for the repository field.
func (r *queryResolver) Repository(ctx context.Context, name string, owner string) (*model.Repository, error) {
// 1. ユーザー名からユーザーIDを取得するサービス層のメソッドを呼ぶ
user, err := r.Srv.GetUserByName(ctx, owner)
if err != nil {
return nil, err
}
// 2. ユーザーIDとレポジトリ名から、レポジトリ詳細情報を取得するサービス層のメソッドを呼ぶ
return r.Srv.GetRepoByFullName(ctx, user.ID, name)
}
レスポンスを作る際のキーとなる部分は、サービス層のGetRepoByFullName
メソッドを実行している部分です。
しかしこのGetRepoByFullName
メソッドの中で取得しているのは、DBに用意されたrepositories
テーブル中のデータのみにしています。
func (r *repoService) GetRepoByFullName(ctx context.Context, owner, name string) (*model.Repository, error) {
repo, err := db.Repositories(
qm.Select(
db.RepositoryColumns.ID, // レポジトリID
db.RepositoryColumns.Name, // レポジトリ名
db.RepositoryColumns.Owner, // レポジトリを所有しているユーザーのID
db.RepositoryColumns.CreatedAt, // 作成日時
),
db.RepositoryWhere.Owner.EQ(owner),
db.RepositoryWhere.Name.EQ(name),
).One(ctx, r.exec)
if err != nil {
return nil, err
}
return convertRepository(repo), nil
}
func convertRepository(repo *db.Repository) *model.Repository {
return &model.Repository{
ID: repo.ID,
Owner: &model.User{ID: repo.Owner},
Name: repo.Name,
CreatedAt: repo.CreatedAt,
}
}
サービス層の全コード(GraphQLのuserクエリ・repositoryクエリ実行に必要な部分)
type UserService interface {
GetUserByID(ctx context.Context, id string) (*model.User, error)
GetUserByName(ctx context.Context, name string) (*model.User, error)
}
type RepoService interface {
GetRepoByFullName(ctx context.Context, owner, name string) (*model.Repository, error)
}
type services struct {
*userService
*repoService
}
func New(exec boil.ContextExecutor) Services {
return &services{
userService: &userService{exec: exec},
repoService: &repoService{exec: exec},
}
}
type userService struct {
exec boil.ContextExecutor
}
func (u *userService) GetUserByName(ctx context.Context, name string) (*model.User, error) {
user, err := db.Users(
qm.Select(db.UserTableColumns.ID, db.UserTableColumns.Name),
db.UserWhere.Name.EQ(name),
// qm.Where("name = ?", name),
).One(ctx, u.exec)
if err != nil {
return nil, err
}
return convertUser(user), nil
}
type repoService struct {
exec boil.ContextExecutor
}
func (r *repoService) GetRepoByFullName(ctx context.Context, owner, name string) (*model.Repository, error) {
repo, err := db.Repositories(
qm.Select(
db.RepositoryColumns.ID,
db.RepositoryColumns.Name,
db.RepositoryColumns.Owner,
db.RepositoryColumns.CreatedAt,
),
db.RepositoryWhere.Owner.EQ(owner),
db.RepositoryWhere.Name.EQ(name),
).One(ctx, r.exec)
if err != nil {
return nil, err
}
return convertRepository(repo), nil
}
func convertRepository(repo *db.Repository) *model.Repository {
return &model.Repository{
ID: repo.ID,
Owner: &model.User{ID: repo.Owner},
Name: repo.Name,
CreatedAt: repo.CreatedAt,
}
}
本来ならばテーブルのjoinなどを行って、レポジトリに紐づいたIssueやPRの情報を取得するべきなのですがそれを行っていないため、DBのrepositories
テーブル内にある情報しかレスポンスに含めることができないのです。
GraphQLのRepository オブジェクトのフィールド |
サービス層で取得し紐付けているデータ |
---|---|
id | repositoriesテーブルのid列 |
name | repositoriesテーブルのname列 |
createdAt | repositoriesテーブルのcreated_at列 |
owner | N/A(オーナーとなるユーザーIDはrepositoriesテーブルのowner列から取れているが、それだけでは不足している) |
issue | N/A |
issues | N/A |
pullRequest | N/A |
pullRequests | N/A |
次章予告
リゾルバの実装(応用編)のリゾルバ分割の実装と動作確認について次のPartで説明いたします。
今日は以上です。
よろしくお願いいたします。