🌟

Ent+Atlasでマイグレーションする

2022/12/02に公開
2

Makuake Advent Calendar 2022の2日目の記事です。

goのORMのEntとDBスキーマ管理ツールのAtlasで、マイグレーションの管理を試してみたので手順や注意ポイントを書いていこうと思います。

今回の作業内容はこちらのリポジトリです。
https://github.com/renoinn/ent_atlas

EntとAtlasについて

Ent

FacebookのConnectivityチームが開発したgo製のORMです。グラフ構造としてモデル化できたり、コード生成で静的型付けしていて型安全などの特徴があります。
コマンドで生成するのであんまり気にならないですが、いろんなファイルがたくさん作られるので、最初ちょっとビックリしました。

Atlas

go製のデータベーススキーマ管理ツールです。terraformなどで使われているhclでDBのスキーマを表現します。
terraformのようにdry-runで差分を確認しながら適用できたりするのは面白いです。inspectコマンドで既存のDBからhclに書き出したりもできます。

Entではマイグレーションのバージョン管理にAtlasを利用するようになっていて、EntのモデルからAtlas向けのsqlファイルを生成して適用できます。

エンティティの作成

というわけで、実際にやっていきましょう。まずはUsersを作成してカラムを追記します。

mkdir ent_atlas
cd ent_atlas
go mod init
go run -mod=mod entgo.io/ent/cmd/ent init Users
ent/schema/user.go
package schema

import (
	"entgo.io/ent"
	"entgo.io/ent/schema/field"
)

// Users holds the schema definition for the Users entity.
type Users struct {
	ent.Schema
}

// Fields of the Users.
func (Users) Fields() []ent.Field {
    return []ent.Field{
        field.String("name"),
        field.String("email").Unique(),
    }
}

// Edges of the Users.
func (Users) Edges() []ent.Edge {
	return nil
}

マイグレーションのバージョン管理を有効にしたいので、generate.goに--feature sql/versioned-migrationのオプションを追加してgo generateします。

ent/generate.go
 package ent

- //go:generate go run -mod=mod entgo.io/ent/cmd/ent generate ./schema
+ //go:generate go run -mod=mod entgo.io/ent/cmd/ent generate --feature sql/versioned-migration ./schema
go generate ./ent

これで./ent以下にEntファイルがたくさん生成されます。

Atlasでマイグレーションの実施

次にAtlasで読み込むためのSQLファイルの生成をしましょう。
SQL生成にはそれ用の実行ファイルを用意する必要があるので、cmd/migrationにmain.goを作成します。

cmd/migration/main.go
package main

import (
	"context"
	"fmt"
	"log"
	"os"

	atlas "ariga.io/atlas/sql/migrate"
	"entgo.io/ent/dialect"
	"entgo.io/ent/dialect/sql/schema"
	_ "github.com/go-sql-driver/mysql"
	"github.com/renoinn/ent_atlas/ent/migrate" // <- ここは自分のプロジェクトのent/migrateパッケージを指定します
)

func main() {
    dataSourceName := fmt.Sprintf("mysql://%s:%s@%s/%s?charset=utf8&parseTime=True", "sample_user", "sample_password", "localhost:3306", "sample_db")
    ctx := context.Background()

    // Create a local migration directory able to understand Atlas migration file format for replay.
    dir, err := atlas.NewLocalDir("ent/migrate/migrations")
    if err != nil {
        log.Fatalf("failed creating atlas migration directory: %v", err)
    }

    // Migrate diff options.
    opts := []schema.MigrateOption{
        schema.WithDir(dir),                         // provide migration directory
        schema.WithMigrationMode(schema.ModeInspect), // provide migration mode
        schema.WithDialect(dialect.MySQL),           // Ent dialect to use
        schema.WithFormatter(atlas.DefaultFormatter),
    }

    // Generate migrations using Atlas support for MySQL (note the Ent dialect option passed above).
    err = migrate.NamedDiff(ctx, dataSourceName, os.Args[1], opts...)
    if err != nil {
        log.Fatalf("failed generating migration file: %v", err)
    }
}

公式のサンプルだとMigrationModeがModeReplayになってますが、空っぽのDBに対してのみ実行可能な状態になってしまって、後からALTER TABLEしたりできないのでModeInspectにしました。
また、NewLocalDir()に渡してるディレクトリのパスは事前に作っておきましょう。

mkdir -p ent/migrate/migrations

SQL生成用の実行ファイルを作ったら、これを使ってSQLファイルを生成します。

go run -mod=mod ./cmd/migration/main.go create_users
  • ent/migrate/migrations/20221118170131_create_users.sql
  • ent/migrate/migrations/atlas.sum

コマンドを実行するとSQLファイルとatlas.sumファイルが作成されました。これらをAtlasで読み込んでマイグレーションを実施していきます。

マイグレーションの適用

Ent的には生成されたSQLをAtlasのCLIで実行するのがオススメらしいので、atlasコマンドで適用します。

atlas migrate apply \
  --dir "file://ent/migrate/migrations" \
  --url mysql://sample_user:sample_password@localhost:3306/sample_db

Migrating to version 20221118170131 (1 migrations in total):

  -- migrating version 20221118170131
    -> CREATE TABLE `users` (`id` bigint NOT NULL AUTO_INCREMENT, `name` varchar(255) NOT NULL, `email` varchar(255) NOT NULL, PRIMARY KEY (`id`), UNIQUE INDEX `email` (`email`)) CHARSET utf8mb4 COLLATE utf8mb4_bin;
  -- ok (68.290735ms)

  -------------------------
  -- 74.369822ms
  -- 1 migrations
  -- 1 sql statements

これでDBにusersテーブルが作成されました。ちなみに、atlas migrate applyの後ろに--dry-runオプションを渡すことで、実行前の確認ができます。

atlas.sumについて

atlas.sumはマイグレーションの競合を検出しやすくするためのファイルで、中身はハッシュが書かれています。
2つのチームが別々のブランチでマイグレーションを更新している場合、先にマージした方のatlas.sumに対して、後からマージする側がコンフリクトすることになるので、この段階で両チームがマイグレーションに変更を加えようとしてることを検出できるようになるというものです。

atlas.sum
h1:XByVwY0M7STzDgo4CUYtJH/k893KZXJWmbbv4jHkjx0=
20221118170131_create_users.sql h1:waz0+8hwYJBKtLU7GnZs4laMr0oZjBtbpQhSpCvFs5Y=

生成されたファイルに後から手動で変更を加えていないかどうかは下記のコマンドでチェックできます。

atlas migrate validate --dir file://<path-to-your-migration-directory>

マイグレーションの追加

Usersにカラムを追加して、ALTER TABLEするようなマイグレーションを追加してみましょう。

ent/schema/users.go
func (Users) Fields() []ent.Field {
    return []ent.Field{
        field.String("name"),
        field.String("email").Unique(),
+	field.String("introduction"),
    }
}

先ほどと同じように、generateしてSQLファイルを生成します。

go generate ./ent
go run -mod=mod ./cmd/migration/main.go add_introduction

すると下記のような内容のsqlファイルが生成されるので、これをapplyします。

20221118170355_add_introduction.sql
-- modify "users" table
ALTER TABLE `users` ADD COLUMN `introduction` varchar(255) NOT NULL;
atlas migrate apply \
  --dir "file://ent/migrate/migrations" \
  --url mysql://sample_user:sample_password@localhost:3306/sample_db
  
Migrating to version 20221118170355 from 20221118170131 (1 migrations in total):

  -- migrating version 20221118170355
    -> ALTER TABLE `users` ADD COLUMN `introduction` varchar(255) NOT NULL;
  -- ok (50.788009ms)

  -------------------------
  -- 57.206188ms
  -- 1 migrations
  -- 1 sql statements

これでintroductionカラムがusersテーブルに追加されました。
このあたりの履歴はatlas_schema_revisionsテーブルに保存されていきます。

+----------------+------------------+------+---------+-------+---------------------+----------------+-------+------------+----------------------------------------------+-----------------------------------------------------+---------------------------------+
| version        | description      | type | applied | total | executed_at         | execution_time | error | error_stmt | hash                                         | partial_hashes                                      | operator_version                |
+----------------+------------------+------+---------+-------+---------------------+----------------+-------+------------+----------------------------------------------+-----------------------------------------------------+---------------------------------+
| 20221118170131 | create_users     |    2 |       1 |     1 | 2022-11-18 17:02:20 |        6914999 |       |            | waz0+8hwYJBKtLU7GnZs4laMr0oZjBtbpQhSpCvFs5Y= | ["h1:Kj9sZH8fYYIPlRVQ5rpgxUTiHffSTvYSoxWLRLQuEXQ="] | Atlas CLI v0.8.2-302c1e6-canary |
| 20221118170355 | add_introduction |    2 |       1 |     1 | 2022-11-18 17:06:14 |        6612335 |       |            | N3yQSGNq7neIdnojDTdSL2PJQ3iKKOR/nQ+1bj4+YC8= | ["h1:izPjxQI3bzAg2p0p58xWX+/4YN0r730f3fT0+CNd4L8="] | Atlas CLI v0.8.2-302c1e6-canary |
+----------------+------------------+------+---------+-------+---------------------+----------------+-------+------------+----------------------------------------------+-----------------------------------------------------+---------------------------------+

まとめ

EntとAtlasを使ったマイグレーションの流れについて、実際にやってみたことをまとめてみました。
Atlasの方はgoのアプリケーションじゃなくても単体で導入できるので、フレームワークに搭載されているマイグレーション機能で上手くいっていない場合や、アプリケーションとスキーマ管理を分離したい場合などに検討してみる価値があるように思いました。atlas schema inspectで既存DBのスキーマをhclファイルとして書き出せるのも良いですね。
Entの方はマイグレーションだけじゃなく実際にアプリケーションを組んでみないと何ともですが、今回のようにマイグレーション管理はAtlas側に任せておけば後からORMだけ変えたりもしやすいので、Atlas前提になっている点はとても良いなと思いました。
EntがAtlasを取り入れたのが2022年の3月頃なので、これからまだまだ進化しそうで期待しています。

おまけ:一度マイグレーション用のSQLファイルを生成した後に、フィールドなどを書き換える場合

生成されたSQLを見てみて、「やっぱりこうしよう」みたいな感じで修正したくなることがあります。
エンティティを修正してgenerateするところまでは同じ手順で大丈夫ですが、そのままSQLファイルを生成すると上書きはせずに新規で生成してしまうので、修正前のSQL+修正後のSQLの両方が存在してしまいます。

atlas.sumの中身はこんな感じになってしまいます。

h1:ot0oyFCmaDgVv5CexXOAnjDBlxjBWm6ypxeBtryxswY=
20221108025948_create_todo.sql h1:P3lh3alKiSWJyatvaBPlRsv108Nj3gdzNOtB13Dn4HQ=
20221108030147_create_todo.sql h1:P3lh3alKiSWJyatvaBPlRsv108Nj3gdzNOtB13Dn4HQ=

この状態でapplyしようとしてもおかしなことになるので、まずは不要な方のSQLファイルを削除します。

rm -rf ent/migrate/migrations/20221108025948_create_todo.sql

削除したら、atlas.sumを編集して削除した方のSQLファイルの行を削除します。

h1:ot0oyFCmaDgVv5CexXOAnjDBlxjBWm6ypxeBtryxswY=
- 20221108025948_create_todo.sql h1:P3lh3alKiSWJyatvaBPlRsv108Nj3gdzNOtB13Dn4HQ=
20221108030147_create_todo.sql h1:P3lh3alKiSWJyatvaBPlRsv108Nj3gdzNOtB13Dn4HQ=

削除したらhashを生成し直すコマンドを実行します。

atlas migrate hash --dir "file://ent/migrate/migrations"

applyコマンドに--dry-runオプションを付けて確認して問題なければ適用しましょう。

参考リンク

https://tech.techtouch.jp/entry/ent-atlas-migration
https://entgo.io/ja/docs/versioned-migrations
https://entgo.io/ja/blog/2022/03/14/announcing-versioned-migrations

Discussion

nabeyangnabeyang

Ent触ったときにAtlasのことは知っていたのですが、全然調べてなかったのでこの記事を参考に試してみようと思いました。ありがとうございます。

あ、あと誤植と思われるところがありました。
次にAtlasで読み込むのSQLファイルの生成をしましょう。
=>
次にAtlasで読み込むためのSQLファイルの生成をしましょう。

renoinnrenoinn

コメントありがとうございます〜!参考になれば幸いです!
ご指摘の箇所修正しました。