メインコンテンツへスキップする

データマイグレーション

マイグレーションは、通常、データベーススキーマを移行するために使用されますが、場合によっては、 データベースに格納されているデータ を移行する必要があります。 たとえば、シードデータを追加したり、空のカラムをカスタムデフォルト値で埋め戻しします。

この種のマイグレーションはデータマイグレーションと呼ばれます。 このドキュメントでは、Entを使用してデータマイグレーションを計画し、通常のスキーママイグレーションのワークフローに統合する方法について説明します。

マイグレーションの種類

Ent currently supports two types of migrations, versioned migration and declarative migration (also known as automatic migration). データマイグレーションはどちらのマイグレーションでも実行可能です。

バージョン管理型マイグレーション

バージョン管理型マイグレーションを使用する場合、データマイグレーションは同じ migrations ディレクトリに保存し、通常のマイグレーションと同じように実行します。 ただし、簡単にテストできるように、データマイグレーションとスキーママイグレーションを別々のファイルに保存することをオススメします。

このようなマイグレーションに使用される形式は SQL です。なぜなら、Ent スキーマが変更され、生成されたコードがデータマイグレーションファイルと互換性がなくなっても、ファイルを安全に実行できる (そして変更せずに保存できる) からです。

データマイグレーションスクリプトの作成方法には、手動と自動生成の2種類があります。 手動で編集すると、ユーザーはすべての SQL 文を書き、何が実行されるかを正確に制御することができます。 また、Entを使用して、データマイグレーションを生成することもできます。 場合によっては手動で修正・編集する必要があるため、自動生成されたファイルが正しく生成されたかを確認することをお勧めします。

手動での作成

1. Atlasをインストールしていない場合は、 getting-startedガイドをチェックしてください。

2. Atlasを使って、新しいマイグレーションファイルを作成します。

atlas migrate new <migration_name> \
--dir "file://my/project/migrations"

3. マイグレーションファイルを編集し、そこにカスタムデータマイグレーションを追加します。 例:

ent/migrate/migrations/20221126185750_backfill_data.sql
-- NULLまたはnullのtagsをデフォルト値でバックフィルします
UPDATE `users` SET `tags` = '["foo","bar"]' WHERE `tags` IS NULL OR JSON_CONTAINS(`tags`, 'null', '$');

4. マイグレーションディレクトリのintegrity fileを更新します。

atlas migrate hash \
--dir "file://my/project/migrations"

データマイグレーションファイルのテスト方法がわからない場合は、以下の テスト セクションを参照してください。

スクリプトを生成する

現在、Ent はデータマイグレーションファイルの生成に対応しています。 このオプションを使用することで、ユーザーは、ほとんどの場合、複雑な SQL 文を手動で記述するプロセスを簡略化できます。 それでも、一部のエッジケースでは手動で編集する必要があるため、生成されたファイルが正しく生成されたことの確認が推奨されます。

1. バージョン管理型マイグレーションのセットアップ

2. 最初のデータマイグレーション関数を作成します。 以下に、関数の書き方を示すいくつかの例を示します。

ent/migrate/migratedata/migratedata.go
package migratedata

// BackfillUnknown back-fills all empty users' names with the default value 'Unknown'.
func BackfillUnknown(dir *migrate.LocalDir) error {
w := &schema.DirWriter{Dir: dir}
client := ent.NewClient(ent.Driver(schema.NewWriteDriver(dialect.MySQL, w)))

// Change all empty names to 'unknown'.
err := client.User.
Update().
Where(
user.NameEQ(""),
).
SetName("Unknown").
Exec(context.Background())
if err != nil {
return fmt.Errorf("failed generating statement: %w", err)
}

// Write the content to the migration directory.
return w.FlushChange(
"unknown_names",
"Backfill all empty user names with default value 'unknown'.",
)
}

Then, using this function in ent/migrate/main.go will generate the following migration file:

migrations/20221126185750_unknown_names.sql
-- Backfill all empty user names with default value 'unknown'.
UPDATE `users` SET `name` = 'Unknown' WHERE `users`.`name` = '';

3. 生成されたファイルを編集した場合、以下のコマンドでマイグレーションディレクトリ整合性ファイルを更新する必要があります:

atlas migrate hash \
--dir "file://my/project/migrations"

テスト

マイグレーションファイルを追加した後、それらが有効であり、意図した結果を達成することを確認するために、ローカルのデータベース上でそれらを適用することを強くオススメします。 以下のプロセスは、手動またはプログラムによる自動化で行うことができます。

1. 最後に作成したデータマイグレーションファイルまで、すべてのマイグレーションファイルを実行します:

# ファイル数
number_of_files=$(ls ent/migrate/migrations/*.sql | wc -l)

# 最後のファイル以外のすべてのファイルを実行する
atlas migrate apply $[number_of_files-1] \
--dir "file://my/project/migrations" \
-u "mysql://root:pass@localhost:3306/test"

2. 最後のマイグレーションファイルが実行保留中であることを確認する

atlas migrate status \
--dir "file://my/project/migrations" \
-u "mysql://root:pass@localhost:3306/test"

Migration Status: PENDING
-- Current Version: <VERSION_N-1>
-- Next Version: <VERSION_N>
-- Executed Files: <N-1>
-- Pending Files: 1

3. データマイグレーションファイルを実行する前に、ローカルデータベースに本番データベースを表す一時的なデータを入れる。

4. atlas migrate apply を実行し、正常に実行されたことを確認する。

atlas migrate apply \
--dir "file://my/project/migrations" \
-u "mysql://root:pass@localhost:3306/test"

なお、atlas schema cleanを使用することで、ローカル開発に使用しているデータベースをクリーンアップし、データマイグレーションファイルが目的の結果を得るまでこのプロセスを繰り返すことができます。

自動マイグレーション

In the declarative workflow, data migrations are implemented using Diff or Apply Hooks. これは、バージョン型マイグレーションとは異なり、自動マイグレーションは適用時に名前もバージョンも保持しないからです。 Therefore, when a data is written using hooks, the type of the schema.Change must be checked before its execution to ensure the data migration was not applied more than once.

func FillNullValues(dbdialect string) schema.ApplyHook {
return func(next schema.Applier) schema.Applier {
return schema.ApplyFunc(func(ctx context.Context, conn dialect.ExecQuerier, plan *migrate.Plan) error {
// データマイグレーションのトリガーとなるschema.Changeを検索します
hasC := func() bool {
for _, c := range plan.Changes {
m, ok := c.Source.(*schema.ModifyTable)
if ok && m.T.Name == user.Table && schema.Changes(m.Changes).IndexModifyColumn(user.FieldName) != -1 {
return true
}
}
return false
}()
// Changeが見つかった場合は、データマイグレーションを適用します
if hasC {
// At this stage, there are three ways to UPDATE the NULL values to "Unknown".
// この段階で、NULL 値を "Unknown" に 更新する方法は 3 つあります。
// カスタム migrate.Change を migrate.Plan に追加する、SQL 文を dialect.ExecQuerier で直接実行する、または生成された ent.Client を使用する。

// マイグレーションコネクションから一時的なクライアントを作成します
client := ent.NewClient(
ent.Driver(sql.NewDriver(dbdialect, sql.Conn{ExecQuerier: conn.(*sql.Tx)})),
)
if err := client.User.
Update().
SetName("Unknown").
Where(user.NameIsNil()).
Exec(ctx); err != nil {
return err
}
}
return next.Apply(ctx, conn, plan)
})
}
}

詳細については、 Apply Hook の例のセクションを参照してください。