GqlgenでDB(postgreSQL)の導入
間違っている部分、エラーが発生する部分、わかりにくい部分などがございましたら、是非ご指摘をお願い致します。
今回はPart3になります。
基本的な事をメインに行っているので前回までの内容を知らなくても基本的には問題ありません。前回までの内容をgithubから取得して来ても良いかと思います。(importのgithub.comを調整する必要があるかも知れないですが...)
前回:公式ドキュメントを参考にGraphqlのスケルトンプロジェクトを作成いたしました。
この記事の内容
前回作成した公式のドキュメントで作成したプロジェクトにデータベースを導入していきます。
- migrationファイルの作成
- gormを用いたデータベースの操作
Github Repo
この記事で紹介するコードはGithubに置いています。
Part3までの切り取り
最初にdockerを起動します。
$ docker-compose start
Starting db ... done
Starting server ... done
データベースにテーブルを作成する
golang-migrateを使用していきます。
golang-migrateは、Goで書かれたデータベースマイグレーションです。
それでは、使用していきます。
今回もCLIは基本的にコンテナの中で実行していくことを前提としています。なので適宜docker-compose exec コンテナ名
をつけてコマンドを実行してください。
まずローカルにgolang-migrateをインストールします。
$ brew install golang-migrate
理由としましては、migrateが実行されるDBコンテナがGoのコンテナとは別のためローカルにインストールする必要があります。
インストールが完了したら、ローカルに下記コマンドを入力してmigrationファイルの作成を行います。
$ migrate create -ext sql -dir db/migrations -seq create_users_table
$ migrate create -ext sql -dir db/migrations -seq create_todos_table
コマンドの入力が完了すると下記のようなディレクトリ構成が生成されます。
├── db
│ └── migrations
│ ├── 000001_create_users_table.down.sql
│ ├── 000001_create_users_table.up.sql
│ ├── 000002_create_todos_table.down.sql
│ └── 000002_create_todos_table.up.sql
それでは、upにはテーブルの作成(バージョンUp),downにはテーブルの削除(バージョンDown)を行うような処理を記述していきます。
schema.graphqlsのtype User
をを参考にしながら作成していきます。
BEGIN;
CREATE TABLE IF NOT EXISTS users(
id VARCHAR (255) UNIQUE NOT NULL PRIMARY KEY,
name VARCHAR (255)
);
CREATE INDEX on users(id);
COMMIT;
upなのでテーブルの作成処理を記載しています。
idをPRIMARY KEYに設定しています。そして、idにインデックスを付けています。
DROP TABLE IF EXISTS users;
downなのでテーブルの削除処理を記載しています。
BEGIN;
CREATE TABLE IF NOT EXISTS todos(
id VARCHAR (255) UNIQUE NOT NULL PRIMARY KEY,
text TEXT,
done BOOL DEFAULT FALSE,
user_id VARCHAR (255)
);
CREATE INDEX on todos(id);
COMMIT;
今回も同様に作成していきます。注目すべき点は、user_idの部分かなと思います。ここは、schema上ではuserになっていたと思いますが、テーブルの中にテーブルを埋め込むことは出来ないのでuser_idを設定することでテーブルの結合をできるようにするために設定しています。
DROP TABLE IF EXISTS todos;
downなのでテーブルの削除処理を記載しています。
migrationファイルの実行
ローカルの環境で下記のコマンドを実行します。
$ migrate -database="postgres://postgres:password@localhost:5432/todo?sslmode=disable" -path db/migrations up
1/u create_todos (42.56058ms)
2/u create_users (88.483438ms)
実行するとtodoDB内にテーブルが作成されます。
migrate -database="{DATABASE_URL}" -path db/migrations up
DATABASE_URL=postgres:{DBのuser名}:{DBのpassword}@{ホストのアドレス}:{ポート番号}/{DB名}?sslmode=disable
Dockerを作成した際にuser名とパスワードを入力してください。
このようなコマンドを入力することでpostgreSQL内にテーブルを作成することが出来ます。
テーブルの確認を行います。
今回はTable Plusを使用します。基本的に無料で使え私個人も無料分で満足して使うことが出来ています。
インストールが完了したらcreate a new connection
-> PostgreSQL
->create
と操作します。
(私が入力した値)
Name= 任意の名前 (gqlgen-todos)
host= 127.0.0.1 (127.0.0.1)
user=データベースのユーザ名 (postgres)
password=データベースのパスワード (password)
port=5432 (5432)
そしてログインを接続すると
schema_migrations,todos,usersと言う3つのテーブルが作成されています。
これでテーブルの作成の確認ができました。
gqlgenでデータベースを使用する
今回は、Gormと言うORMを使用してデータベースを操作していきます。
個人的には、ORMを使用することでより直感的にRDBの操作をすることができると思います。
db/connect.goを作成します。そして、データベースへの接続処理を記述していきます。
.envファイルなどの環境変数の設定は後ほど行っていきます。
package db
import (
"gorm.io/driver/postgres"
"gorm.io/gorm"
"os"
)
func ConnectGORM() *gorm.DB {
//データベースへの接続
dsn := os.Getenv("DATABASE_URL")
db, err := gorm.Open(postgres.Open(dsn), &gorm.Config{})
if err != nil {
panic(err.Error())
}
return db
}
そして、gormをまだインポートしていないのでコンテナの中で下記コマンドを入力してダウンロードします。
$ go mod tidy
データベースへの接続処理をGoサーバーへ反映させていきます。
package graph
//go:generate go run github.com/99designs/gqlgen generate
// This file will not be regenerated automatically.
//
// It serves as dependency injection for your app, add any dependencies you require here.
import (
"gorm.io/gorm" //追加
)
type Resolver struct{
DB *gorm.DB //変更
}
Resolver内でデータベースを扱えるようにするためにResolverの構造体にDBを定義しています。
こうすることでschema.resolverなどで DBを扱えるようになります。また、todosが消えたため一時的にschema.resolverでエラーが発生しますがスルーして大丈夫です。
.envの設定と先ほど宣言したResolver構造体にDBを渡していく処理を記述していきます。
package main
import (
"log"
"net/http"
"os"
"fmt"
"github.com/99designs/gqlgen/graphql/handler"
"github.com/99designs/gqlgen/graphql/playground"
"github.com/shion0625/gqlgen-todos/graph"
"github.com/shion0625/gqlgen-todos/db" //追加
"github.com/joho/godotenv" //追加
)
const defaultPort = "8080"
func main() {
loadEnv() //追加
port := os.Getenv("PORT")
if port == "" {
port = defaultPort
}
//データベースへの接続処理
db := db.ConnectGORM() //追加
// resolver内でデータベースを扱えるように設定
srv := handler.NewDefaultServer(graph.NewExecutableSchema(graph.Config{Resolvers: &graph.Resolver{
// resolver.goで宣言した構造体にデータベースの値を受け渡し
DB: db, // 追加
}}))
http.Handle("/", playground.Handler("GraphQL playground", "/query"))
http.Handle("/query", srv)
log.Printf("connect to http://localhost:%s/ for GraphQL playground", port)
log.Fatal(http.ListenAndServe(":"+port, nil))
}
// ここで.envファイル全体を読み込みます。
// この読み込み処理がないと、個々の環境変数が取得出来ません。
func loadEnv() {
// 読み込めなかったら err にエラーが入ります。
err := godotenv.Load(".env")
if err != nil {
fmt.Printf("読み込み出来ませんでした: %v", err)
}
}
loadEnv関数で環境変数を取得処理を記述しています。
godotenvでエラーが出てると思うので下記コマンドを実行してダウンロードを行います。
$go mod tidy
環境変数の設定を行います。
自身の環境に合わせて設定を行なってください。
DATABASE_URL=host=db user=postgres password=password dbname=todo
port=5432 sslmode=disable TimeZone=Asia/Tokyo
PORT=8080
データベースを操作する
schema.resolversのtodosでメモリで管理していた部分をデータベースでの管理にしていきます。
func (r *mutationResolver) CreateTodo(ctx context.Context, input model.NewTodo) (*model.Todo, error) {
//ランダムな数字の生成
rand, _ := rand.Int(rand.Reader, big.NewInt(100))
todo := model.Todo{
Text: input.Text,
ID: fmt.Sprintf("T%d", rand),
UserId: input.UserID,
}
r.DB.Create(&todo)
return &todo, nil
}
// Todos is the resolver for the todos field.
func (r *queryResolver) Todos(ctx context.Context) ([]*model.Todo, error) {
todos := []*model.Todo{}
r.DB.Find(&todos)
return todos, nil
}
このように書き換えます。しかし、エラーが発生していると思います。 これは、Todoモデルの中はUserIdでは無くUserが必要だからです。しかし、ここでUserのオブジェクトであったり作成処理をしてしまうとオブジェクトの作成、取得にコストがかかってしまいます。queryに関しても現在のままではTodoしか取得できずユーザを取得することが出来ません。このqueryの中でユーザの取得処理を記述するとユーザーをクライアントが必要としない時も呼び出してしまいコストがかかってしまいます。
その問題を解決していきます。
# Where are all the schema files located? globs are supported eg src/**/*.graphqls
schema:
- graph/*.graphqls
# Where should the generated server code go?
exec:
filename: graph/generated.go
package: graph
# Uncomment to enable federation
# federation:
# filename: graph/federation.go
# package: graph
# Where should any generated models go?
model:
filename: graph/model/models_gen.go
package: model
# Where should the resolver implementations go?
resolver:
layout: follow-schema
dir: graph
package: graph
# Optional: turn on use ` + "`" + `gqlgen:"fieldName"` + "`" + ` tags in your models
# struct_tag: json
# Optional: turn on to use []Thing instead of []*Thing
# omit_slice_element_pointers: false
# Optional: turn off to make struct-type struct fields not use pointers
# e.g. type Thing struct { FieldA OtherThing } instead of { FieldA *OtherThing }
# struct_fields_always_pointers: true
# Optional: turn off to make resolvers return values instead of pointers for structs
# resolvers_always_return_pointers: true
# Optional: set to speed up generation time by not performing a final validation pass.
# skip_validation: true
# gqlgen will search for any type names in the schema in these go packages
# if they match it will use them, otherwise it will generate them.
autobind:
- "github.com/shion0625/gqlgen-todos/graph/model"
# This section declares type mapping between the GraphQL and go type systems
#
# The first line in each type will be used as defaults for resolver arguments and
# modelgen, the others will be allowed when binding to fields. Configure them to
# your liking
models:
ID:
model:
- github.com/99designs/gqlgen/graphql.ID
- github.com/99designs/gqlgen/graphql.Int
- github.com/99designs/gqlgen/graphql.Int64
- github.com/99designs/gqlgen/graphql.Int32
Int:
model:
- github.com/99designs/gqlgen/graphql.Int
- github.com/99designs/gqlgen/graphql.Int64
- github.com/99designs/gqlgen/graphql.Int32
Todo:
fields:
user:
resolver: true
autobindとmodels下にTodoを追加いたしました。
次にgraph/model/todo.goを作成します。
そして、graph/model/models_gen.goからからtype TodoをコピーしてきてUserの部分を変更いたします。
package model
type Todo struct {
ID string `json:"id"`
Text string `json:"text"`
Done bool `json:"done"`
- User *User `json:"user"`
+ UserId string `json:"user"`
}
下記のコマンドを実行してコードを再生成します。
$ go generate ./...
もし使用できない方は、下記コードをそのままresolver.goに追加してください
//go:generate go run github.com/99designs/gqlgen generate
再生性が完了するとschema.resolvers.go
にUser resolverが追加されていると思います。
user := model.User{ID: obj.UserId}
r.DB.First(&user)
return &user, nil
UserIdが一致するものを取得してくるという処理になっています。
こうすることでGraphQLがUserが必要な時にこのUser resolverを呼び出すことで無駄にデータベースを呼ぶことなどをせずに必要な時だけ呼び出されるようになります。
$ go run server.go
それでは、下記コマンドを実行します。
userのnameに値が何も入っていないと思います。これはcreateTodoではTodoの作成について焦点を当てているためユーザの名前の登録などは別のmutationでするべき事柄だからです。
mutation createTodo {
createTodo(input:{text: "gqlgen", userId:"graphql"}){
id,
text,
user{
id,
name
}
}
}
queryも実行
query Todos {
todos {
text
user {
name
}
}
}
まとめ
今回はtodoの情報をメモリに保存していた部分をデータベースへ変更いたしました。
- migration
- Gormの使用
- 環境変数の設定
を行ってきました。次回では、cerateUserのmutationの追加とuserから複数のtodoを取得をするといった処理を実装していきます。
次回part4の記事