🕌
[gqlgen]Go + PostgresQLでGraphQLを実装してみる
はじめに
Goの勉強がてらGraphQLで何かを実装してみたかったので、やってみました。
GraphQLのライブラリは色々あるっぽいのですが、スキーマベースで実装したかったのでgqlgenで実装することにしました。
極力生のgoを勉強するためにフレームワークやORMは使ってません。
細かい導入方法はチュートリアルや他の記事を参照して頂ければと思います。
Dockerで環境構築
チュートリアルだと構造体に値を入れるだけだったので、ちゃんとしたアプリ用にデータベースに値を保存することにしました。
そのうちherokuに何かデプロイしたかったのでDBはpostgresにしました。
Dockerfile
FROM golang:latest
ENV APP_ROOT /app
RUN mkdir $APP_ROOT
WORKDIR $APP_ROOT
COPY ./ $APP_ROOT
EXPOSE 8080
CMD ["go", "run", "main.go"]
docker-compose.yml
version: '3.7'
services:
db:
image: postgres:latest
container_name: go-postgresql
environment:
POSTGRES_HOST_AUTH_METHOD: trust
POSTGRES_DB: db
ports:
- '5432:5432'
volumes:
- /var/lib/postgresql/data
server:
build:
context: .
dockerfile: Dockerfile
depends_on:
- db
ports:
- '8080:8080'
volumes:
- .:/app
これで立ち上げたdbにusersテーブルを作成してください。
個人的にはTablePlusをDBクライアントにおすすめしてます。
//これを先に実行
CREATE EXTENSION IF NOT EXISTS "uuid-ossp";
//次にこれを実行
CREATE TABLE users
(
id uuid PRIMARY KEY DEFAULT uuid_generate_v4(),
name varchar(30) not null,
);
ロジック実装
フォルダ構成は以下のようになっています。
server
|- /graph
| |- /generated - generated.go //自動生成されたファイル
| |- /model - models_gen.go //自動生成されたファイル
| |- resolver.go
| |- schema.graphqls
| |- schema.resolvers.go
|- docker-compose.yml
|- Dockerfile
|- gqlgen.yml
|- main.go
main.goにdb接続とサーバー立ち上げの処理を書きます。
依存モジュールの管理はModulesを使いました。
main.go
package main
import (
"database/sql"
"log"
"net/http"
"os"
"github.com/99designs/gqlgen/graphql/handler"
"github.com/99designs/gqlgen/graphql/playground"
"github.com/[リポジトリ]/graph"
"github.com/[リポジトリ]/graph/generated"
_ "github.com/lib/pq"
)
const defaultPort = "8080"
func main() {
// DBへの接続
// host.docker.internalはコンテナをホストしてるipを指定してます
// docker-composeでネットワークを作った方が良いとは思います。
// なんか分からないけどうまく行かなかったのでとりあえずこうしてます。
db, err := sql.Open("postgres", "host=host.docker.internal port=5432 user=postgres dbname=db sslmode=disable")
if err != nil {
log.Fatal(err)
}
defer db.Close()
port := os.Getenv("PORT")
if port == "" {
port = defaultPort
}
// Resolverに接続したdbを渡してResolverでdbにアクセスできるようにします。(Resolver.goに構造体を追加します)
srv := handler.NewDefaultServer(generated.NewExecutableSchema(generated.Config{Resolvers: &graph.Resolver{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))
}
スキーマを作成します。
これはチュートリアル通りです(自動生成)。
schema.graphqls
type Todo {
id: ID!
text: String!
done: Boolean!
user: User!
}
type User {
id: ID!
name: String!
}
type Query {
todos: [Todo!]!
}
input NewTodo {
text: String!
userId: String!
}
type Mutation {
createTodo(input: NewTodo!): Todo!
}
リゾルバーのエンドポイントを作成します。
これも基本的にチュートリアル通りです(自動生成)。
ですが、CreateUserだけ実装してみます。
schema.resolvers.go
package graph
// This file will be automatically regenerated based on the schema, any resolver implementations
// will be copied through when generating and any unknown code will be moved to the end.
import (
"context"
"fmt"
"github.com/[リポジトリ]/graph/generated"
"github.com/[リポジトリ]/graph/model"
)
func (r *mutationResolver) CreateUser(ctx context.Context, input model.NewUser) (*model.User, error) {
// 追加
// 実際の中身はresolver.goに実装
return r.createUser(input)
}
func (r *mutationResolver) AddBook(ctx context.Context, input model.NewBook) (*model.Book, error) {
panic(fmt.Errorf("not implemented"))
}
func (r *queryResolver) Books(ctx context.Context) ([]*model.Book, error) {
panic(fmt.Errorf("not implemented"))
}
// Mutation returns generated.MutationResolver implementation.
func (r *Resolver) Mutation() generated.MutationResolver { return &mutationResolver{r} }
// Query returns generated.QueryResolver implementation.
func (r *Resolver) Query() generated.QueryResolver { return &queryResolver{r} }
type mutationResolver struct{ *Resolver }
type queryResolver struct{ *Resolver }
実際にUserを作成する処理をResolverに追加していきます。
resolver.go
package graph
import (
"database/sql"
"fmt"
"time"
"github.com/[リポジトリ]/graph/model"
_ "github.com/lib/pq"
)
type Resolver struct{
// 追加
DB *sql.DB
}
// 追加
func (r *Resolver) createUser(input model.NewUser) (*model.User, error){
// dbにuserを保存する処理
cmd := "INSERT INTO users (name) VALUES ($1)"
_, err := r.DB.Exec(cmd, name)
if err != nil {
return nil, err
}
// 省略しますが、dbに保存されたデータをここで取得して、idとnameという変数に保存
var user model.User = model.User{
ID: id,
Name: name,
}
return &user, nil
}
これでUserが作成されて保存できたと思います。
まとめ
どのファイルに何を書くかとか、引数に何を渡すかとか色々つまったけどなんとかできて良かったです。
実際に動いたものを確認したものは構造体の名前とか異なるので、もし上記の例で動かなかったら教えてください。
Discussion