Go × PostgreSQL × SQLBoiler (ORM) × golang-migrate (マイグレーション) の開発環境を構築する

毎回環境を作るときに何かしらド忘れしてしまい手こずってしまうので忘れないようにメモ。

# はじめに
- 下記の知識や経験があることを前提にしてます
  - Go言語の基礎を理解している
  - Dockerと docker-compose の基礎を理解している
  - バックエンド開発やデータベースの基礎を理解している
- ORMの使い方や各種コマンドの詳しい説明はしません
- パスなどはご自身のプロジェクトの構成に合わせて適宜変更してください

作ったもののサンプルです。

https://github.com/Takenari-Yamamoto/golang-dev-env/tree/main

# 開発環境
- Go1.20
- PostgreSQL 15
- Docker
- docker-compose

# 使用するツール

### SQLBoler　v3.7.1 

https://github.com/volatiletech/sqlboiler

### golang-migrate　v4.15.2

https://github.com/golang-migrate/migrate

# 手順① Docker と docker-compose で Go と PostgreSQL を起動

## Dockerfile作成
### Go
```Dockerfile
FROM golang:1.20

WORKDIR /app

RUN go install github.com/volatiletech/sqlboiler@latest
RUN go install github.com/volatiletech/sqlboiler/drivers/sqlboiler-psql@latest
RUN go install -tags 'postgres' github.com/golang-migrate/migrate/v4/cmd/migrate@v4.15.2

# この辺はプロジェクトのディレクトリ構成に合わせてください
COPY ./src .
```

- SQLBoiler と golang-migrate もインストールしておきます
- ORMのコマンドとマイグレーションのコマンドもDocker上で叩けるようにします

### PostgreSQL
```Dockerfile
FROM postgres:15
```

## docker-compose.yml作成

```yml
version: "3.7"

services:
  app:
    build:
      context: .
      dockerfile: ./docker/golang/Dockerfile
    volumes:
      - ./:/app
    command: ["go", "run", "src/cmd/main.go"]
    ports:
      - "8080:8080"
    environment:
      - DB_HOST=${DB_HOST:-db}
      - DB_PORT=${DB_PORT:-5432}
      - DB_USER=${POSTGRES_USER:-postgres}
      - DB_PASSWORD=${POSTGRES_PASSWORD:-password}
      - DB_NAME=${POSTGRES_DB:-postgres}
    depends_on:
      - db
    networks:
      - app_network

  db:
    build:
      context: .
      dockerfile: ./docker/postgresql/Dockerfile
    ports:
      - "5432:5432"
    volumes:
      - postgres_data:/var/lib/postgresql/data
    environment:
      - POSTGRES_USER=${POSTGRES_USER:-postgres}
      - POSTGRES_PASSWORD=${POSTGRES_PASSWORD:-password}
      - POSTGRES_DB=${POSTGRES_DB:-postgres}
    networks:
      - app_network

volumes:
  postgres_data:

networks:
  app_network:
```

## 起動してみる
### エントリーポイントとなる main.go を作成
```go
package main

import (
	"database/sql"
	"fmt"
	"log"
	"net/http"
	"os"

	_ "github.com/lib/pq"
)

func main() {

	// DB接続
	dsn := fmt.Sprintf(
		"host=%s user=%s password=%s dbname=%s port=%s sslmode=disable TimeZone=Asia/Tokyo",
		os.Getenv("DB_HOST"),
		os.Getenv("DB_USER"),
		os.Getenv("DB_PASSWORD"),
		os.Getenv("DB_NAME"),
		os.Getenv("DB_PORT"),
	)
	db, err := sql.Open("postgres", dsn)
	if err != nil {
		log.Fatal("failed to init database: ", err)
	}

	err = db.Ping()
	if err != nil {
		log.Fatal("failed to connect database: ", err)
	}

	log.Default().Println("success to connect db!!")

	// HTTPサーバーを起動
	http.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
		w.Write([]byte("Hello, World!"))
	})

	err = http.ListenAndServe(":8080", nil)
	if err != nil {
		log.Fatal("failed to serve: ", err)
	}
	log.Default().Println("Server started on port: 8080")
}

```

### envファイル作成
- ルートディレクトリに.envファイルを作成し、下記を追加
```env
DB_HOST=db
DB_PORT=5432
DB_USER=postgres
DB_PASSWORD=password
DB_NAME=sample
```


- ここで設定した環境変数は docker-compose で起動する時に読み取ってくれます

```yml
# docker-compose で設定したやつ。
 environment:
      - DB_HOST=db
      - DB_PORT=5432
      - DB_USER=${POSTGRES_USER:-postgres}
      - DB_PASSWORD=${POSTGRES_PASSWORD:-password}
      - DB_NAME=${POSTGRES_DB:-postgres}
```


### docker起動
```bash
docker-compose up -d --build
```
docker-desktop で下記を確認してください
- http server が起動できてること
- DBが起動してること

# 手順② マイグレーションファイルの作成
## golang-migate のコマンドを叩いてマイグレーションファイルを生成
-  下記のように実行できます
```bash
docker-compose exec app migrate create -ext sql -dir ${ファイルを生成する先のディレクトリ} -seq ${テーブル名}
```

- 実行するとこんな感じです
```bash
docker-compose exec app migrate create -ext sql -dir ./src/database/migrations -seq tasks
```

- 実行に成功したらマイグレーションファイルが生成されてるはずです。

```bash
src
 ├── database
    │   ├── migrations
    │   │   ├── 000001_tasks.down.sql
    │   │   └── 000001_tasks.up.sql
```

## マイグレーションファイルに記述
### upファイル
```sql
CREATE TABLE tasks (
    id varchar(36) PRIMARY KEY,
    title TEXT NOT NULL,
    created_at TIMESTAMP NOT NULL
);
```

### downファイル
```sql
DROP TABLE IF EXISTS tasks;
```

# 手順③ マイグレーションファイルの内容をDBに反映する

```bash
docker-compose exec app migrate -database "postgres://${ユーザー名}:${パスワード}@${ホスト}:${ポート}/${データベースの名前}?sslmode=disable" -path ${マイグレーションファイルのパス} up
```
こんな感じ
```bash
docker-compose exec app migrate -database "postgres://postgres:password@db:5432/postgres?sslmode=disable" -path ./src/database/migrations up
```

ここまでできるとテーブルが作成されてるはずです。

- PostgreSQL の中に入る
```bash
docker-compose exec db psql --host=localhost --port=5432 --username=postgres
```
- `\l` でデータベース一覧を確認
```bash
postgres=# \l
                                                          List of databases
   Name    |  Owner   | Encoding |  Collate   |   Ctype    | ICU Locale | Locale Provider |   Access privileges   
-----------+----------+----------+------------+------------+------------+-----------------+-----------------------
 postgres  | postgres | UTF8     | en_US.utf8 | en_US.utf8 |            | libc            | 
 sample    | postgres | UTF8     | en_US.utf8 | en_US.utf8 |            | libc            | 
 template0 | postgres | UTF8     | en_US.utf8 | en_US.utf8 |            | libc            | =c/postgres          +
           |          |          |            |            |            |                 | postgres=CTc/postgres
 template1 | postgres | UTF8     | en_US.utf8 | en_US.utf8 |            | libc            | =c/postgres          +
           |          |          |            |            |            |                 | postgres=CTc/postgres
(4 rows)
```

- （もしデータベースを自分で作成していたら） `\c ${作成したデータベースの名前}` のコマンドを叩いてデータベースを切り替えてください

```bash
postgres=# \c sample
You are now connected to database "sample" as user "postgres".
sample=# 
```

- `/dt` でテーブル一覧を表示する

```bash
sample=# \dt
               List of relations
 Schema |       Name        | Type  |  Owner   
--------+-------------------+-------+----------
 public | schema_migrations | table | postgres
 public | tasks             | table | postgres
(2 rows)
```

- `\d ${テーブル名}`で作成されたフィールドの一覧を確認

```bash
sample=# \d tasks
                           Table "public.tasks"
   Column   |            Type             | Collation | Nullable | Default 
------------+-----------------------------+-----------+----------+---------
 id         | character varying(36)       |           | not null | 
 title      | text                        |           | not null | 
 created_at | timestamp without time zone |           | not null | 
Indexes:
    "tasks_pkey" PRIMARY KEY, btree (id)
```


- 上記のように作成したテーブルとフィールドが存在していたら成功です。

ちなみに down と force のコマンドは下記のとおりです。

down
```bash
docker-compose exec app migrate -database "postgres://${ユーザー名}:${パスワード}@${ホスト}:${ポート}/${データベースの名前}?sslmode=disable" -path ${マイグレーションファイルのパス} down
```

force
```bash
docker-compose exec app migrate -database "postgres://${ユーザー名}:${パスワード}@${ホスト}:${ポート}/${データベースの名前}?sslmode=disable" -path ${マイグレーションファイルのパス} force ${バージョン}
```

# 手順④ DBのスキーマからGoのコードを生成する（ORM）

SQLBoilerを使ってDBスキーマからコードを自動生成します。

## tomlファイルの作成
- SQLBoiler設定ファイルを作成します
- ` sqlboiler.toml` というファイルをルートに作成して、DBの設定に応じて下記を追加してください

```toml
[psql]
  dbname = "sample"
  host   = "db"
  port   = 5432
  user   = "postgres"
  pass   = "password"
  sslmode = "disable"
  schema = "public"
```

## SQLBoilerのコマンドを叩いてスキーマからコード生成
- 下記のようなフォーマットで生成できます
```bash
docker-compose exec app sqlboiler psql --output ${出力先のパス名} --pkgname models --wipe
```
- 今回は下記のようにします。
```bash
docker-compose exec app sqlboiler psql --output ./src/database/models --pkgname models --wipe
```

実行に成功すると指定したディレクトリにコードが生成されます。
```bash
└── src
    ├── cmd
    │   └── main.go
    └── database
        ├── migrations
        │   ├── 000001_tasks.down.sql
        │   └── 000001_tasks.up.sql
        └── models // DBのスキーマから自動生成されたファイル
            ├── boil_main_test.go
            ├── boil_queries.go
            ├── boil_queries_test.go
            ├── boil_suites_test.go
            ├── boil_table_names.go
            ├── boil_types.go
            ├── psql_main_test.go
            ├── psql_suites_test.go
            ├── psql_upsert.go
            ├── schema_migrations.go
            ├── schema_migrations_test.go
            ├── tasks.go
            └── tasks_test.go

```

生成後は `go mod tidy`を忘れず実行しましょう。
```bash
go mod tidy
```

- 生成したコードを使ってDBにアクセスできます。

```go
package repository

import (
	"context"
	"database/sql"
	"github/Takenari-Yamamoto/golang-dev-env/src/database/models" // SQLBoilerから生成されたモデル

	"github.com/volatiletech/sqlboiler/boil"
)

// 全件取得
func GetAllTasks(ctx context.Context, db *sql.DB) (tasks []*models.Task, err error) {
	res, err := models.Tasks().All(ctx, db)
	if err != nil {
		return nil, err
	}
	return res, nil
}

// 1件取得
func GetTask(ctx context.Context, db *sql.DB, id string) (task *models.Task, err error) {
	res, err := models.FindTask(ctx, db, id)
	if err != nil {
		return nil, err
	}
	return res, nil
}

// 作成
func CreateTask(ctx context.Context, db *sql.DB, task *models.Task) (err error) {
	err = task.Insert(ctx, db, boil.Infer())
	if err != nil {
		return err
	}
	return nil
}

// 更新
func UpdateTask(ctx context.Context, db *sql.DB, task *models.Task) (err error) {
	_, err = task.Update(ctx, db, boil.Infer())
	if err != nil {
		return err
	}
	return nil
}

// 1件削除
func DeleteTask(ctx context.Context, db *sql.DB, task *models.Task) (err error) {
	_, err = task.Delete(ctx, db)
	if err != nil {
		return err
	}
	return nil
}

```

# おまけ
Makefileで実行できるようにしておくと便利です。
```makefile
create-migration-file:
	docker-compose exec app migrate create -ext sql -dir ./src/database/migrations -seq ${TABLE_NAME}

migrate-up:
	docker-compose exec app migrate -database "postgres://postgres:password@db:5432/sample?sslmode=disable" -path ./src/database/migrations up

migrate-down:
	docker-compose exec app migrate -database "postgres://postgres:password@db:5432/sample?sslmode=disable" -path ./src/database/migrations down

migrate-force:
	docker-compose exec app migrate -database "postgres://postgres:password@db:5432/sample?sslmode=disable" -path ./src/database/migrations force ${VERSION}

gen-code-from-db:
	docker-compose exec app sqlboiler psql --output ./src/database/models --pkgname models --wipe && cd src && go mod tidy
```