1
1

Goで学ぶGraphQLサーバーサイド(6)ーリゾルバの実装(応用編)1

Posted at

こんにちは。

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

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

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で説明いたします。

今日は以上です。

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

1
1
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
1
1