LoginSignup
1
0

More than 1 year has passed since last update.

gqlgen 1対多のデータの取得【実装編-Part3】

Last updated at Posted at 2022-12-31

1対多のデータの取得

間違っている部分、エラーが発生する部分、わかりにくい部分などがございましたら、是非ご指摘をお願い致します。

今回はPart4になります。

基本的な事をメインに行っているので前回までの内容を知らなくても基本的には問題ありません。前回までの内容をgithubから取得して来ても良いかと思います。(importのgithub.comを調整する必要があるかも知れないですが...)

前回:公式ドキュメントを用いて作成した物にデータベースの導入を行いました。

この記事の内容

前回までに作成Todoアプリを改修していきます。

  • createUserのmutationの追加
  • usersのqueryを追加
  • usersからそのユーザの複数のTodoを取得する

Github Repo

この記事で紹介するコードはGithubに置いています。

Part4までの切り取り

最初にdockerを起動します。

$ docker-compose start
Starting db     ... done
Starting server ... done

スキーマファイルを更新する

スキーマファイルにMutation、Query、inputの追加を行っていきます。

  • 追加点
    • createUser: createTodoではユーザ名の登録を行うのは不適切なため、新しく-mutationを用意してユーザの作成を行います。
    • users: 全てのuserの一覧を取得します。
    • NewUser: ユーザを作成する時に必要な情報を定義しています。
  • 変更点
    • user: 複数のTodoを取得できるように変更
graph/schema.graphqls
type Todo {
  id: ID!
  text: String!
  done: Boolean!
  user: User!
}

type User {
  id: ID!
  name: String!
+  todos: [Todo!]!
}

type Query {
  todos: [Todo!]!
+  users: [User!]!
}

input NewTodo {
  text: String!
  userId: String!
}

+ input NewUser {
+   name: String!
+ }

type Mutation {
  createTodo(input: NewTodo!): Todo!
+  createUser(input: NewUser!): User!
}

ここまでの変更が完了したら今回も基本的にはコンテナ内でコマンドを打つことをそうしてしておりますのでdocker-compose exec コンテナ名を適宜つけてコマンドを実行してください。

$ go generate ./...

新しくコードが生成されたら、models_gen.goのtype Userの中にTodoが存在しているのですがデータベース上では存在していないので、今回も外部にモデルを切り出してtypeに変更を加えていきます。

graph/model/user.goを作成します。

graph/model/user.go
package model

type User struct {
	ID    string  `json:"id"`
	Name  string  `json:"name"`
- 	Todos []*Todo `json:"todos"`
}

Userの型にTodosがあるままだとリゾルバーで処理を実装する際に戻り値にTodosを含める必要があり、Userの取得処理の中にtodoの取得処理も混ざってしまい無駄なデータの取得をすることになってしまいます。GraphQLの必要な情報を取得するといったコンセプトから外れてしまいます。

gqlgen.yml
models:
...
  Todo:
    fields:
      user:
        resolver: true
+  User:
+    fields:
+      todo:
+        resolver: true

yamlに変更を加えたのでもう一度コードの再生成をします。

$ go generate ./...

最終的にschema.resolvers.goに追加されたコードは下記の通りになっていると思います。
既に作成してあるtodosとは別にfunc (r *userResolver) Todosが作成されている事を確認してください。

graph/schema.resolvers.go
// CreateUser is the resolver for the createUser field.
func (r *mutationResolver) CreateUser(ctx context.Context, input model.NewUser) (*model.User, error) {
	panic(fmt.Errorf("not implemented: CreateUser - createUser"))
}

// Users is the resolver for the users field.
func (r *queryResolver) Users(ctx context.Context) ([]*model.User, error) {
	panic(fmt.Errorf("not implemented: Users - users"))
}

// Todos is the resolver for the todos field.
func (r *userResolver) Todos(ctx context.Context, obj *model.User) ([]*model.Todo, error) {
	panic(fmt.Errorf("not implemented: Todos - todos"))
}

それでは、この3つのresolverの機能を実装していきます。

graph/schema.resolvers.go
// CreateUser is the resolver for the createUser field.
func (r *mutationResolver) CreateUser(ctx context.Context, input model.NewUser) (*model.User, error) {
	//ランダムな値の生成
	rand, _ := rand.Int(rand.Reader, big.NewInt(100))
	user := model.User{
		ID:     fmt.Sprintf("U%d", rand),
		Name:   input.Name,
	}
	r.DB.Create(&user)
	return &user, nil
}

// Users is the resolver for the users field.
func (r *queryResolver) Users(ctx context.Context) ([]*model.User, error) {
	user := []*model.User{}
	r.DB.Find(&user)
	return user, nil
}

// Todos is the resolver for the todos field.
func (r *userResolver) Todos(ctx context.Context, obj *model.User) ([]*model.Todo, error) {
	todo := []*model.Todo{}
	r.DB.Where("user_id = ?", obj.ID).Find(&todo)
	return todo, nil
}

CreateUser: はランダムな数値をIDに割り当てて、入力されたNameをデータベースに保存しています。
users: は全てのuserを取得しています。最初に戻り値の型に合わせてuserを定義しています。その後全てのuserの取得を行っています。
Todos: は指定されたユーザIDをもつTodoを全て取得しています。

mutationの実行

Graphqlサーバを起動します。

$ go run server.go

それでは、ユーザの作成を行います。

mutation createUser  {
  createUser(input:{name: "graphql"}){
    id,
    name
  }
}

ユーザの作成ができたらその戻り値のIDをコピーしておくかデータベースの中を覗いてUserのIDを確認してください。

それでは、先ほどメモしたユーザのユーザIDを設定してTodoを作成してみましょう
このリゾルバは変更していないので問題なく作成することが出来たと思います。

mutation createTodo  {
  createTodo(input:{text: "todo", userId:"U21"}){
    id,
    text,
    user{
      id,
      name
    }
  }
}

それでは、最終確認です。下記のクエリを実行して、それぞれのuserに紐づいたtodoをすべて取得できれば今回の記事での目的は達成です。

query users  {
  users{
    id,
    todos{
      id,
      text
    }
  }
}

現在の実装の問題点

現在の実装には問題があります。(他にも問題があればご教授お願いします。💦)

N+1問題

N+1問題と言ったものがございます。これは、SQLが一回のリクエストに対して沢山実行されているといった問題です。
それでは、確認してみましょう!
r.DBという部分に続けてDebug()という関数をつけてみてください。
下のは一例です。 他のgormの処理を記述しているところにも付けてみて下さい。どういったSQL文が発行されているかを見ることが出来ます。

graph/schema.resolvers.go
// Todos is the resolver for the todos field.
func (r *userResolver) Todos(ctx context.Context, obj *model.User) ([]*model.Todo, error) {
	todo := []*model.Todo{}
    r.DB.Debug().Where("user_id = ?", obj.ID).Find(&todo) //変更点
	return todo, nil
}

この状況でサーバを再起動して先程のQueryのusersなどを実行してみて下さい。
多くのSQLが発行されている事が確認出来るはずです。

2022/12/31 09:35:54 /app/graph/schema.resolvers.go:65
[6.096ms] [rows:0] SELECT * FROM "todos" WHERE user_id = 'U93'

2022/12/31 09:35:54 /app/graph/schema.resolvers.go:65
[1.829ms] [rows:0] SELECT * FROM "todos" WHERE user_id = 'U12'

2022/12/31 09:35:54 /app/graph/schema.resolvers.go:65
[49.961ms] [rows:0] SELECT * FROM "todos" WHERE user_id = 'U3'

2022/12/31 09:35:54 /app/graph/schema.resolvers.go:65
[45.902ms] [rows:0] SELECT * FROM "todos" WHERE user_id = 'U87'

2022/12/31 09:35:54 /app/graph/schema.resolvers.go:65
[52.091ms] [rows:0] SELECT * FROM "todos" WHERE user_id = 'U38'

2022/12/31 09:35:54 /app/graph/schema.resolvers.go:65
[58.972ms] [rows:0] SELECT * FROM "todos" WHERE user_id = 'U1'

2022/12/31 09:35:54 /app/graph/schema.resolvers.go:65
[61.057ms] [rows:2] SELECT * FROM "todos" WHERE user_id = 'U21'

2022/12/31 09:35:54 /app/graph/schema.resolvers.go:65
[63.121ms] [rows:0] SELECT * FROM "todos" WHERE user_id = 'U47'

2022/12/31 09:35:54 /app/graph/schema.resolvers.go:65
[65.210ms] [rows:0] SELECT * FROM "todos" WHERE user_id = 'U49'

まとめ

今回は1対多のデータの取得をメインに据えて実装を行ってきました。
その中で、N+1問題といった問題が浮き出てきました。この問題はGraphQLを使用していくうえでは決して逃れられない問題です。
次回はこの問題をDataloaderを用いて解決していきます。

次回Part5です。

1
0
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
0