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

2024/03/07に公開

はじめに

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

https://techblog.enechain.com/entry/bun-atlas-migration-setup-guide#モデルの定義以外は全て自動化したい

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
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のモデルからスキーマを生成するために、スクリプトを書きます。
スクリプトは冒頭で紹介した記事の通りです。

// 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コネクションのタイミングで行います。

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に以下コマンドを準備します。

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

コマンドを実行すると...

$ make generate-schema

generate.goで指定したパスに、sqlが生成されました!

Atlasで自動でマイグレーションファイルを生成する

マイグレーションファイル生成コマンド準備

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

実際のコマンドは以下になります。

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コマンドを実行すると...

$ make generate-migration

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

golang-migrateでmigrateする

なぜgolang-migrateを使うのか?

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

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

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

コマンド

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は以下になります。

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

以下を実行すると...

make auto-migrate

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

まとめ

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

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

参考

https://techblog.enechain.com/entry/bun-atlas-migration-setup-guide#モデルの定義以外は全て自動化したい

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

https://atlasgo.io/versioned/diff

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

Discussion