この記事は、LayerX Tech Advent Calendar 2022 7日目の記事です。
Enabling Team エンジニアの @yyoshiki41(中川佳希)です!
直近チームで、 ORMフレームワークである ent を使う機会があり、 そこで利用している Atlas というデータベースマイグレーションエンジンについての紹介です。
ent
ent の詳細な説明は省略しますが、以下のような特徴があります。
- Go 用の ORM フレームワーク
- Go で Schema を記述し、コード生成を行う
- コード生成により、静的型付けされた明示的なAPIを実現
- アプリケーションの中で扱いやすいようモデリングした Schema とそのリレーションを定義可能
- e.g.)
Edges
で、エンティティ間のリレーションを表現
- e.g.)
- Eager Loading, Hook, Privacy レイヤなどの拡張も実装可能
Database Migration
データベースのテーブル定義をどうやって管理していくかは継続的なサービス開発には必須のテーマです。 選択肢も豊富で以下のようなものが思い浮かびます。
- WebフレームワークやORMが提供するツールを利用する
- プログラミング言語やフレームワークに依存しないツールを利用する
そして、ツールによってもスキーマをどのように記述するかは様々です。 SQLを直接記述するものから専用のDSL、プログラミング言語のクラス定義を用いるなど多岐にわたります。
どのツールが最善かは開発スタイル、アプリケーションによりますが、今回 ent を利用していく上で採用した開発フローとマイグレーション方法を紹介します。
Migration Flows
複数の環境に対して、別々のスタイルを取っています。
- Declarative Migrations (automatically): ローカル開発環境、テスト実行環境ではプロセス起動時に自動で定義したスキーマへのマイグレーションを行う
- Versioned Migrations: ステージング環境、リリース環境は、コマンド経由で実行タイミングを制御して用意したSQLで差分のマイグレーションを行う
1 で自動でのマイグレーションを採用している理由は、開発者体験の向上のためです。monorepo, マイクロサービスを前提に考えると、複数のデータベースに他の人が行ったマイグレーションを毎回行うのは手間がかかります(そして忘れることもしばしば)。また、(差分変更がどのように行われるか一定の注意は必要ですが)開発する側として宣言的に ent スキーマをかくだけで良いのもストレスが少ない方法です。
ent は、自動でのマイグレーションを行える関数を自動生成します。以下は、ドキュメントにあるサンプルコードです。単体テスト時も TestMain
に書くだけでよいためショートカットが可能です。
package main func main() { // Connect to the database (MySQL for example). client, err := ent.Open("mysql", "root:pass@tcp(localhost:3306)/test") if err != nil { log.Fatalf("failed connecting to mysql: %v", err) } defer client.Close() ctx := context.Background() // Run migration. if err := client.Schema.Create(ctx); err != nil { log.Fatalf("failed creating schema resources: %v", err) } // ... Continue with server start. }
2 で用意したSQLで計画的にマイグレーションを行っている理由は複数あります。まず、アプリケーションデプロイとマイグレーション実行のプロセスを独立にしたい点があります。各プロセスの中断やロールバックの制御が取れないのは運用上不便です。また、実行SQLを確認したい理由もあります。MySQL のような Online DDL の仕組みがあっても、すべての ALTER 文でサポートされるものではないため、オンラインで実行可能かは開発者での判断が必要です。データボリュームに依るところも機械的には判断出来ない部分です。
この 1. Declarative Migrations => 2. Versioned Migrations をシームレスに互換性を持って行う為に利用するのが Atlas です。
Atlas
冒頭でマイグレーションエンジンと書きましたが、非常に多機能で以下のような特徴があります。
- ent がデフォルトで採用しているマイグレーションエンジン
- GoライブラリとしてもCLI としても利用可能
- マイグレーションファイルの作成、Apply、差分チェックなどが出来る
- 複数のマイグレーションツールの解釈(インポート、アウトプット)が出来る e.g.) golang-migrate, goose, flyway, liquibase and dbmate.
ent が生成してくれるマイグレーション実行を行う関数 (client.Schema.Create
) の内部実装には Atlas がライブラリとして利用されています。
今回は例として、ent スキーマから dbmate 用のマイグレーションファイルを生成してみます。
package main import ( "context" "log" atlasmigrate "ariga.io/atlas/sql/migrate" "ariga.io/atlas/sql/sqltool" "entgo.io/ent/dialect" "entgo.io/ent/dialect/sql/schema" "github.com/org/repo/ent/migrate" _ "github.com/go-sql-driver/mysql" ) func main() { ctx := context.Background() // Create a local migration directory able to understand dbmate migration file format for replay. dir, err := sqltool.NewDBMateDir("./migrations") if err != nil { log.Fatalf("failed creating migration directory: %v", err) } // Write atlas.sum file to the local migration directory. sum, err := dir.Checksum() if err != nil { log.Fatalf("failed check atlas.sum: %v", err) } if err := atlasmigrate.WriteSumFile(dir, sum); err != nil { log.Fatalf("failed writing atlas.sum: %v", err) } // Migrate diff options. opts := []schema.MigrateOption{ schema.WithDir(dir), // provide migration directory schema.WithMigrationMode(schema.ModeReplay), // provide migration mode schema.WithDialect(dialect.MySQL), // Ent dialect to use } // Generate migrations using Atlas support for MySQL (note the Ent dialect option passed above). err = migrate.Diff(ctx, "mysql://root:password@localhost:3306/atlas_dev_database", opts...) if err != nil { log.Fatalf("failed generating migration file: %v", err) } }
処理の流れとしては、以下です。
- Versioned Migrations として管理されている既存のマイグレーションファイルを読み込む
- sum ファイルで既存のマイグレーションファイルの状態が正しいかチェック(バージョンが抜けていたり、変更が加わっていないか)
- 既存のマイグレーションと ent スキーマの差分を計算する
- 計算した差分の Versioned Migrations 用のファイルを生成する
最後に、3. の差分を生成している仕組みをみていきます。
Calculating diffs
重要なコンセプトとして、Dev Database と呼ばれるものがあります。 これは Atlas が 既存のマイグレーションファイル を適用するデータベースのことです。空の状態で用意しておけば自動で今ある Versioned Migrations を適用して、現在のデータベーススキーマ(Current State)を再現してくれます。ここから現在のデータベーススキーマ(Current State)と ent スキーマ(Desired State)の差分を計算しています。 Dev Database は特別な状態管理など不要です。Versioned Migrations ディレクトリさえ渡せば、常に現在のデータベーススキーマを再現(Replay モードと呼ばれている)させ、差分計算後はクリーンナップしてくれているため、Docker コンテナだけ準備するだけなのでローカル環境・CI環境への組み込みとの相性も良いです。
開発者はデプロイ前に生成される差分のSQLファイルを新たに Versioned Migrations ディレクトリに追加するだけでよく、差分を手で書く必要がなくなるのは大きなメリットです。 Atlas は他にも実際にデータベース同士でスキーマ比較なども出来ますが、今回はここまでです。次の Advent Calendar の記事もお楽しみください!