Go + Bun  + Atlas でマイグレーション周りを自動化する

# はじめに
本記事は、以下二つの記事を大いに参考にさせて頂きました！🙏

https://techblog.enechain.com/entry/bun-atlas-migration-setup-guide#%E3%83%A2%E3%83%87%E3%83%AB%E3%81%AE%E5%AE%9A%E7%BE%A9%E4%BB%A5%E5%A4%96%E3%81%AF%E5%85%A8%E3%81%A6%E8%87%AA%E5%8B%95%E5%8C%96%E3%81%97%E3%81%9F%E3%81%84

https://zenn.dev/yulog/scraps/94958b03abeb96

その上で、自分なりにカスタマイズした内容や、構築途中に詰まったところを記載していきたいと思います。

# 環境
- Go v1.20.0
- PostgreSQL v15
- Bun v1.1.17
- Atlas v0.19.0
- golang-migrate 
- Docker
- make

ディレクトリ構成はクリーンアーキテクチャ

# 最終的に実現したいこと
コマンド一発で以下を全て実行したい！！！

1. Bunのモデルからスキーマ（sql）を自動で生成する
2. Atlasで自動でマイグレーションファイルを生成する
3. golang-migrateでmigrateする

# やらないこと
- Atlasの概要説明
- Bunの概要説明
- Bunのモデル定義の説明
    - モデル定義はできていることを前提とする
    - 後述するスキーマ生成スクリプトでエラーが発生する場合は、モデル定義がうまくいっていない可能性が大

# ツールのインストール
Dockerで開発を進めるため、コンテナにatlasとgolang-migrateをインストールします。

```Dockerfile
# Dockerfile
FROM golang:1.20.0-alpine

RUN apk add --no-cache postgresql-client git
RUN go install github.com/cosmtrek/air@latest
RUN go install ariga.io/atlas/cmd/atlas@latest
RUN go install -tags 'postgres' github.com/golang-migrate/migrate/v4/cmd/migrate@latest

WORKDIR /app

COPY go.mod go.sum ./
RUN go mod tidy

COPY . .

EXPOSE 8080

CMD ["air"]
```

# Bunのモデルからスキーマ（sql）を自動で生成する
## スキーマ生成のスクリプトの準備
Bunのモデルからスキーマを生成するために、スクリプトを書きます。
スクリプトは冒頭で紹介した[記事](https://techblog.enechain.com/entry/bun-atlas-migration-setup-guide#%E5%AE%A3%E8%A8%80%E7%9A%84%E3%81%AA%E3%82%B9%E3%82%AD%E3%83%BC%E3%83%9E%E3%83%95%E3%82%A1%E3%82%A4%E3%83%AB%E3%82%92%E7%94%9F%E6%88%90%E3%81%97%E3%81%9F%E3%81%84:~:text=%E3%81%A6%E3%81%84%E3%81%8D%E3%81%BE%E3%81%99%E3%80%82-,Go%20%E3%81%A7%E5%AE%9A%E7%BE%A9%E3%81%95%E3%82%8C%E3%81%9F%E3%83%A2%E3%83%87%E3%83%AB%E3%81%8B%E3%82%89%E3%82%B9%E3%82%AD%E3%83%BC%E3%83%9E%E3%83%95%E3%82%A1%E3%82%A4%E3%83%AB%E3%82%92%E8%87%AA%E5%8B%95%E7%94%9F%E6%88%90,-%E3%81%BE%E3%81%9A%E3%81%AF%20Go%20%E3%81%A7)の通りです。

```go
// generate.go
package main

import (
	"log"
	"os"

        // パスは本来はgithub.comから始まりますが、./へ置き換えています。
	postgres "./infrastructure/persistence/postgres/bun"
	"./infrastructure/persistence/postgres/bun/model"
	"github.com/uptrace/bun"
)

func modelsToByte(db *bun.DB, models []interface{}) []byte {
	var data []byte

	for _, model := range models {
		query := db.NewCreateTable().Model(model).WithForeignKeys()

		rawQuery, err := query.AppendQuery(db.Formatter(), nil)
		if err != nil {
			log.Fatal(err)
		}

		data = append(data, rawQuery...)
		data = append(data, ";\n"...)
	}

	return data
}

func indexesToByte(db *bun.DB, idxCreators []model.IndexQueryCreator) []byte {
	var data []byte

	for _, idxCreator := range idxCreators {
		idx := idxCreator(db)

		rawQuery, err := idx.AppendQuery(db.Formatter(), nil)
		if err != nil {
			log.Fatal(err)
		}

		data = append(data, rawQuery...)
		data = append(data, ";\n"...)
	}

	return data
}

func main() {
	db, err := postgres.LoadConfigAndCreateDBConnection()
	if err != nil {
		log.Fatalf("Could not connect to database: %v", err)
	}

	models := []interface{}{
		(*model.User)(nil),
		(*model.Category)(nil),
		(*model.Memo)(nil),
		(*model.Note)(nil),
		(*model.Portfolio)(nil),
		(*model.MemoCategory)(nil),
		(*model.NoteMemo)(nil),
		(*model.PortfolioCategory)(nil),
		(*model.UserCategory)(nil),
	}

	var data []byte
	data = append(data, modelsToByte(db, models)...)
	data = append(data, indexesToByte(db, model.IdxCreators)...)

	os.WriteFile("infrastructure/persistence/postgres/bun/migrate/schema.sql", data, 0777)
}
```

ポイントとしては、スキーマのsql生成を見越して、リレーションが必要なモデルは後に定義する必要があります。
また、多対多の中間テーブルのモデルはRegisterModelをする必要があるため、DBコネクションのタイミングで行います。

```go
func NewDatabaseConnection(config DatabaseConfig) (*bun.DB, error) {
	// PostgreSQL用のDSNフォーマット
	dsn := fmt.Sprintf("postgres://%s:%s@%s:%s/%s?sslmode=disable&TimeZone=Asia/Tokyo",
		config.User, config.Password, config.Host, config.Port, config.Name)

	connector := pgdriver.NewConnector(pgdriver.WithDSN(dsn))
	sqldb := sql.OpenDB(connector)

	// データベース接続をテスト
	if err := sqldb.Ping(); err != nil {
		log.Fatalf("Failed to connect to database: %v", err)
		return nil, err
	}

	db := bun.NewDB(sqldb, pgdialect.New())

	// クエリーフックを追加することで、SQLを実行したクエリーが標準出力される
	db.AddQueryHook(bundebug.NewQueryHook(
		bundebug.WithVerbose(true),
	))

	// 多対多の中間テーブルはここで登録する
	db.RegisterModel((*model.NoteMemo)(nil))
	db.RegisterModel((*model.MemoCategory)(nil))
	db.RegisterModel((*model.NoteMemo)(nil))
	db.RegisterModel((*model.PortfolioCategory)(nil))
	db.RegisterModel((*model.UserCategory)(nil))

	log.Println("Successfully connected to database")

	return db, nil
}

func LoadConfigAndCreateDBConnection() (*bun.DB, error) {
	config, err := config.LoadConfig()

	if err != nil {
		return nil, fmt.Errorf("could not load config: %w", err)
	}

	// データベース接続を作成
	db, err := NewDatabaseConnection(DatabaseConfig{
		Host:     config.Database.Host,
		Name:     config.Database.Name,
		User:     config.Database.User,
		Password: config.Database.Password,
		Port:     config.Database.Port,
	})

	if err != nil {
		return nil, fmt.Errorf("could not connect to database: %w", err)
	}

	return db, nil
}
```

## スキーマの生成
Makefileに以下コマンドを準備します。
```Makefile
generate-schema:
	docker exec -it my-app ash -c "go run ./infrastructure/persistence/postgres/bun/migrate/generate.go"
```
コマンドを実行すると...
```sh
$ make generate-schema
```

generate.goで指定したパスに、sqlが生成されました！
![](https://storage.googleapis.com/zenn-user-upload/4b5f9f88309c-20240307.png)

# Atlasで自動でマイグレーションファイルを生成する
## マイグレーションファイル生成コマンド準備
atlas migrate diff コマンドを利用して、マイグレーションファイルを自動生成します。
このコマンドは、現在のデータベーススキーマと指定されたスキーマファイルまたはディレクトリとの間で差分を計算し、その差分を利用してマイグレーションファイルを生成するコマンドです。

実際のコマンドは以下になります。
```Makefile
generate-migration:
	docker exec -it my-app ash -c "atlas migrate diff migration \
        --dir 'file://infrastructure/persistence/postgres/bun/migrate/migrations?format=golang-migrate' \
        --to 'file://infrastructure/persistence/postgres/bun/migrate/schema.sql' \
        --dev-url 'postgres://postgres:password@my-app-dev-db:5432/my-app-dev?search_path=public&sslmode=disable'"
```

トピックとしては以下です。
- postgreのv16だと上手く動作しなかったためv15にした
- dev-urlはAtlasが差分チェックのために利用する開発DBで、**アプリケーションのDBとは別のクリーンなDBを準備する必要がある**
    - compose.ymlに同じDockerfileを利用した開発用コンテナを追加して対応した
- format=golang-migrateを指定することで、upとdownの二つのマイグレーションファイルを生成してくれる

## マイグレーションファイルの生成
上述のmakeコマンドを実行すると...
```sh
$ make generate-migration
```

migrationsディレクトリに、migrationファイルが生成されました！🎉

![](https://storage.googleapis.com/zenn-user-upload/8ca2b90babbd-20240307.png)

# golang-migrateでmigrateする
## なぜgolang-migrateを使うのか？
Atlasはapplyコマンドがあり、Atlas自身でマイグレーションを実行することができます。
本来Atlasを利用して行いたい運用としては、gitでのバージョン管理を元に、スキーマとマイグレーションファイルを都度生成し実行することで、マイグレーションファイルを意識しなくても良いのが理想かと思います。
ですが、以下の懸念点から今回はgolang-migrateを利用しています。

- 各環境で開発DBが必要
    - マイグレーションファイルをバージョン管理に含めるとしたら、golang-migrateのが使い勝手が良さそうだった
- 開発中にマイグレーションファイルが膨らむ（削除したら都度hashコマンドを打つ必要あり）
- DB変更の履歴が追いづらい

今回はgolang-migrateを組み込んでいますが、今後実際に運用する中でAtlasで完結させる方針に倒す可能性も大いにあるな〜と思っています。
ベストプラクティスではないので、ご自身の開発要件に合わせて使い分けて頂くと良いかと思います。

## コマンド
```Makefile
migrate:
	docker exec -it my-app ash -c "migrate --path infrastructure/persistence/postgres/bun/migrate/migrations --database 'postgresql://postgres:password@my-app-db:5432/my-app?search_path=public&sslmode=disable' -verbose up"

migrate-rollback:
	docker exec -it my-app ash -c "migrate --path infrastructure/persistence/postgres/bun/migrate/migrations --database 'postgresql://postgres:password@my-app-db:5432/my-app?search_path=public&sslmode=disable' -verbose down 1"
```

# コマンド一発で全て実行する
最終的なMakefileは以下になります。

```Makefile
generate-schema:
	docker exec -it my-app ash -c "go run ./infrastructure/persistence/postgres/bun/migrate/generate.go"

generate-migration:
	docker exec -it my-app ash -c "atlas migrate diff migration \
        --dir 'file://infrastructure/persistence/postgres/bun/migrate/migrations?format=golang-migrate' \
        --to 'file://infrastructure/persistence/postgres/bun/migrate/schema.sql' \
		--dev-url 'postgres://postgres:password@my-app-dev-db:5432/my-app-dev?search_path=public&sslmode=disable'"

migrate:
	docker exec -it my-app ash -c "migrate --path infrastructure/persistence/postgres/bun/migrate/migrations --database 'postgresql://postgres:password@my-app-db:5432/my-app?search_path=public&sslmode=disable' -verbose up"

migrate-rollback:
	docker exec -it my-app ash -c "migrate --path infrastructure/persistence/postgres/bun/migrate/migrations --database 'postgresql://postgres:password@my-app-db:5432/my-app?search_path=public&sslmode=disable' -verbose down 1"

migrate-hash:
	docker exec -it my-app ash -c "atlas migrate hash \
		--dir 'file://infrastructure/persistence/postgres/bun/migrate/migrations'"

auto-migrate:
	make migrate-hash
	make generate-schema
	make generate-migration
	make migrate
```
以下を実行すると...
```sh
make auto-migrate
```

スキーマ作成からマイグレーションの実行まで全て実行されました！🎉

# まとめ
本記事では、Go + Bun　+ Atlas でマイグレーション周りを自動化する方法をまとめてみました！
この仕組みを利用することで、開発者はモデル定義に注力でき、マイグレーション周りの煩わしさが大幅に削減されたと感じています。

この記事が役に立ったと思ったらぜひいいねをお願いします👍
近い将来新しいサービスをリリースする予定なので、ぜひフォローをしてお待ち頂けますと幸いです🫶

# 参考
https://techblog.enechain.com/entry/bun-atlas-migration-setup-guide#%E3%83%A2%E3%83%87%E3%83%AB%E3%81%AE%E5%AE%9A%E7%BE%A9%E4%BB%A5%E5%A4%96%E3%81%AF%E5%85%A8%E3%81%A6%E8%87%AA%E5%8B%95%E5%8C%96%E3%81%97%E3%81%9F%E3%81%84

https://zenn.dev/yulog/scraps/94958b03abeb96

https://atlasgo.io/versioned/diff

https://bun.uptrace.dev/guide/relations.html