[gqlgen]Go + PostgresQLでGraphQLを実装してみる

5 min読了の目安(約4800字TECH技術記事

はじめに

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が作成されて保存できたと思います。

まとめ

どのファイルに何を書くかとか、引数に何を渡すかとか色々つまったけどなんとかできて良かったです。
実際に動いたものを確認したものは構造体の名前とか異なるので、もし上記の例で動かなかったら教えてください。