Go + Bun + Atlas でマイグレーション周りを自動化する
はじめに
本記事は、以下二つの記事を大いに参考にさせて頂きました!🙏
その上で、自分なりにカスタマイズした内容や、構築途中に詰まったところを記載していきたいと思います。
環境
- Go v1.20.0
- PostgreSQL v15
- Bun v1.1.17
- Atlas v0.19.0
- golang-migrate
- Docker
- make
ディレクトリ構成はクリーンアーキテクチャ
最終的に実現したいこと
コマンド一発で以下を全て実行したい!!!
- Bunのモデルからスキーマ(sql)を自動で生成する
- Atlasで自動でマイグレーションファイルを生成する
- 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 でマイグレーション周りを自動化する方法をまとめてみました!
この仕組みを利用することで、開発者はモデル定義に注力でき、マイグレーション周りの煩わしさが大幅に削減されたと感じています。
この記事が役に立ったと思ったらぜひいいねをお願いします👍
近い将来新しいサービスをリリースする予定なので、ぜひフォローをしてお待ち頂けますと幸いです🫶
参考
Discussion