Goで学ぶGraphQLサーバーサイド（６）ーリゾルバの実装（応用編）１

こんにちは。

Part 6は「リゾルバの実装 - 応用編」についてです。


# この章について

「リゾルバの実装 - 基本編」にて実装を行った際には、GraphQLのスキーマで定義されたクエリ・ミューテーション1つに対してリゾルバメソッド1つが紐づいている状態でした。

- `user`クエリ: `*queryResolver型`の`User`メソッドを実行
- `repository`クエリ: `*queryResolver`型の`Repository`メソッドを実行
- `node`クエリ: `*queryResolver`型のNodeメソッドを実行
- `addProjectV2ItemById`ミューテーション: `*mutationResolver`型のAddProjectV2ItemByIDsメソッドを実行

この章ではリゾルバを分割することによって、この1:1対応を解消していきます。

# リゾルバを分割する前の状況確認

まずは、リゾルバ分割を行っていない状況ではどのような挙動をしているのか、もう一度確認してみましょう。
応用編では、`repository`クエリを例にとって説明していきたいと思います。

## `repository`クエリと得られるレスポンス型

`repository`クエリは、レスポンスとして`Repository`オブジェクトを返すように定義されています。

```schema.graphqls
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オブジェクト

```schema.graphqls
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`クエリを実行して、得られるレポジトリ情報を全て表示させてみようと思います。
そのようなクエリは以下のような形になります。

```go
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
      }
    }
  }
}
```

しかし、基礎編の内容に従って実装していくと、以下のような少しおかしなレスポンスが得られるかと思います。

```json
{
  "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`クエリを実行した際に呼び出されるリゾルバは一つだけです。

```graph/schema.resolvers.go
// 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`テーブル中のデータのみにしています。

```graph/services/repositories.go
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クエリ実行に必要な部分)

```graph/services/service.go
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},
	}
}
```

```graph/services/users.go
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
}
```

```graph/services/repositories.go
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で説明いたします。

今日は以上です。

よろしくお願いいたします。