1
0

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

Posted at

はじめに

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

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

環境

  • 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 でマイグレーション周りを自動化する方法をまとめてみました!
この仕組みを利用することで、開発者はモデル定義に注力でき、マイグレーション周りの煩わしさが大幅に削減されたと感じています。

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

参考

1
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
1
0