【Facebook/ent】ent生成のSQLと自作のSQLを共存させる
ent とは
Facebook 社製の Go のエンティティフレームワーク
https://entgo.io/ja/
ORM なので ent のスキーマを書けば SQL の生成まで行ってくれます。
2種類のマイグレーション
ent にはマイグレの方法が2種類用意されています。
- オートマイグレーション
- https://entgo.io/ja/docs/migrate
- アプリケーション起動時に自動でマイグレを行ってくれる
- バージョン管理型マイグレーション
- https://entgo.io/ja/docs/versioned-migrations
- 一般的なマイグレと同じ。SQL ファイルを生成し、そのファイルに基づいてマイグレを行う
この2種類です。
自作の SQL を差し込みたい
基本的には ent に搭載されている機能で賄えますが、各 SQL に関して全ての機能を網羅しているわけではありません。
時には、ent に未搭載の機能を使いたい時もあるかと思います。
その場合は自作の SQL を作成し、マイグレ時に差し込む必要があります。
上記2種類の各マイグレ方法で、自作と ent 共存に詰まったので共有します。
今回の実装例
今回の実装例として、「PostgreSQL において複数のテーブルで共通のシーケンスを使用したい」というケースを挙げます。
以下のテーブルがあるとします。
account_number
を自動インクリメントにしたい 且つ インクリメントの値は3つのテーブルで共通にしたい
users
, admins
, staffs
に順に 1 つずつレコードを追加したとしたら、
- users
- id: 1
- account_number: 1
- name: ユーザー1
- admins
- id: 1
- account_number: 2
- name: 管理者1
- staffs
- id: 1
- account_number: 3
- name: スタッフ1
こんな感じでデータが入ることを目標にします。
用意する SQL
シーケンスとしてsequence_account_number
を用意します。
各テーブルの生成は ent に任せるので、ここではシーケンスの設定のみ行います。
CREATE SEQUENCE IF NOT EXISTS "sequence_account_number" START WITH 1 MINVALUE 1;
ALTER TABLE "users" ALTER COLUMN "account_number" SET DEFAULT nextval('sequence_account_number');
ALTER TABLE "admins" ALTER COLUMN "account_number" SET DEFAULT nextval('sequence_account_number');
ALTER TABLE "staffs" ALTER COLUMN "account_number" SET DEFAULT nextval('sequence_account_number');
オートマイグレーションでの共存
オートマイグレの時は ent の Apply フックを利用することで、SQL を差し込むことができます。
client, err := ent.Open("postgres", "host=......")
if err != nil {
log.Fatalf("ポスグレへの接続に失敗しました: %v", err)
}
defer client.Close()
// ...
err = client.Schema.Create(
ctx,
schema.WithApplyHook(customApplyHook), // ここでapplyHookの関数を差し込む
)
if err != nil {
log.Fatalf("スキーマ作成に失敗しました: %v", err)
}
func customApplyHook(next schema.Applier) schema.Applier {
return schema.ApplyFunc(func(ctx context.Context, conn dialect.ExecQuerier, plan *atlasMigrate.Plan) error {
// SQLファイルの読み込み
content, err := os.ReadFile("migrate/auto/add_sequence_account_number.sql") // 自作したSQLファイルのパス
if err != nil {
return err
}
// 読み込んだSQLファイルの内容を実行
query := string(content)
err = conn.Exec(ctx, query, []interface{}{}, nil)
if err != nil {
return err
}
return next.Apply(ctx, conn, plan)
})
}
問題点
これで OK!かと思いきや問題点があります。
シーケンスの追加などは元々テーブルが作成されていることが前提なので、
err = client.Schema.Create(
ctx,
schema.WithApplyHook(customApplyHook), // ここでapplyHookの関数を差し込む
)
このタイミングではまだテーブルが作成されておらず、テーブルが見つからないというエラーが発生してしまいます。
解決法
アプリケーションの起動前に一度セットアップとして ApplyHook を除いたclient.Schema.Create()
を行う必要があります。
func main() {
client, err := ent.Open("postgres", "host=......")
if err != nil {
log.Fatalf("ポスグレへの接続に失敗しました: %v", err)
}
defer client.Close()
// ...
err = client.Schema.Create(ctx)
if err != nil {
log.Fatalf("事前のスキーマ作成に失敗しました: %v", err)
}
err = client.Schema.Create(
ctx,
schema.WithApplyHook(customApplyHook),
)
if err != nil {
log.Fatalf("スキーマ作成に失敗しました: %v", err)
}
}
というわけでメイン関数はこんな感じになります。
2回 Create メソッドを使ってるのがなんとも違和感がありますが仕方ないです。
バージョン管理型マイグレーションでの共存
バージョン管理型マイグレーションでは AtlasCLI を使って Ent スキーマに定義された migration ファイルを生成します。
※ドキュメントに載っているので生成方法については割愛
migrate/version
ディレクトリに SQL ファイルを生成します。
↓ のような SQL ファイルとatlas.sum
ファイルが生成されていると思います。
// migrate/version/202312021751_first_migration.sql
CREATE TABLE "users" (
"id" BIGINT PRIMARY KEY,
"name" VARCHAR(255),
"account_number" BIGINT,
"deleted_at" TIMESTAMP,
"created_at" TIMESTAMP,
"updated_at" TIMESTAMP
);
CREATE TABLE "admins" (
"id" BIGINT PRIMARY KEY,
"name" VARCHAR(255),
"account_number" BIGINT,
"deleted_at" TIMESTAMP,
"created_at" TIMESTAMP,
"updated_at" TIMESTAMP
);
CREATE TABLE "staffs" (
"id" BIGINT PRIMARY KEY,
"name" VARCHAR(255),
"account_number" BIGINT,
"deleted_at" TIMESTAMP,
"created_at" TIMESTAMP,
"updated_at" TIMESTAMP
);
// migrate/version/atlas.sum
h1:vj6fBSDiLEwe+jGdHQvM2NU8G70lAfXwmI+zkyrxMnk=
202312021751_first_migration.sql h1:wrm4K8GSucW6uMJX7XfmfoVPhyzz3vN5CnU1mam2Y4c=
では、自作の SQL ファイルもここに追加します。
新しい migration ファイルの生成は以下のコマンドでできます。
atlas migrate new add_sequence_account_number --dir "file://migrate/version"
migrate/version/202312021800_add_sequence_account_number.sql
こんな感じでファイルが作成されていると思うので、SQL を書いていきます。
// migrate/version/202312021800_add_sequence_account_number.sql
CREATE SEQUENCE IF NOT EXISTS "sequence_account_number" START WITH 1 MINVALUE 1;
ALTER TABLE "users" ALTER COLUMN "account_number" SET DEFAULT nextval('sequence_account_number');
ALTER TABLE "admins" ALTER COLUMN "account_number" SET DEFAULT nextval('sequence_account_number');
ALTER TABLE "staffs" ALTER COLUMN "account_number" SET DEFAULT nextval('sequence_account_number');
atlas.sum
でマイグレーションを管理しているため、ハッシュ値を再計算してあげる必要があります。
atlas migrate hash --dir "file://migrate/version"
atlas.sum
が以下のように更新されるかと思います。
// migrate/version/atlas.sum
h1:vj6fBSDiLEwe+jGdHQvM2NU8G70lAfXwmI+zkyrxMnk=
202312021751_first_migration.sql h1:wrm4K8GSucW6uMJX7XfmfoVPhyzz3vN5CnU1mam2Y4c=
202312021800_add_sequence_account_number h1:FO18ZEjsRb6FrWgnSBKsRyDsfIQ5XUTe9POBVvWgdhQ=
問題点
さて、ここで重要なのはent スキーマの内容は変わっていないということです。
ここで、ent スキーマにカラムを追加して再度 migration ファイルを生成してみましょう。
試しに、users テーブルに email カラムを追加してみます。
以下のような SQL が生成されます。
// migrate/version/202312021900_add_users_email_column.sql
ALTER TABLE "users" ALTER COLUMN "account_number" DROP DEFAULT;
ALTER TABLE "admins" ALTER COLUMN "account_number" DROP DEFAULT;
ALTER TABLE "staffs" ALTER COLUMN "account_number" DROP DEFAULT;
ALTER TABLE "users"
ADD COLUMN "email" VARCHAR(255) UNIQUE;
DROP DEFAULT
DROP DEFAULT
DROP DEFAULT
というわけで、ent スキーマは変わらないので、せっかく作ったデフォルト値を削除してしまいます。
解決法
ent のマイグレで参照するディレクトリ
自作の SQL を置くディレクトリ
この2つのディレクトリを別に管理してあげれば解決します。
ただ、実際にマイグレーションを apply する際には2つのディレクトリ両方を参照しなければなりません。
現状、atlas で複数ディレクトリを対象に apply する方法は無いです。
また、atlas.sum
でのハッシュ値も統合する必要があります。
結論は以下です。
- ent のマイグレは
migrate/version/ent
で管理 - 自作の SQL は
migrate/version/custom
に置く - 2つのディレクトリの中のファイルを
migrate/version/all
にコピー- ハッシュ値の再計算
- apply 時はここを参照する
というわけで、スクリプトを書いてあげる必要があります。
// sync_migration_files.sh
#!/bin/bash
# ソースディレクトリ
SRC_DIRS=("./migrate/version/custom" "./migrate/version/ent")
# ターゲットディレクトリ
DEST_DIR="./migrate/version/all"
# ターゲットディレクトリが存在しなければ作成
mkdir -p $DEST_DIR
if [ $? -ne 0 ]; then
echo "ディレクトリの作成に失敗しました: $DEST_DIR"
exit 1
fi
# ファイルをコピー
for src in "${SRC_DIRS[@]}"; do
cp $src/*.sql $DEST_DIR/
if [ $? -ne 0 ]; then
echo "ファイルのコピーに失敗しました。: $src/*.sql -> $DEST_DIR/"
exit 1
fi
done
# allディレクトリに存在して、customやentからは消されたファイルを削除
for file in $DEST_DIR/*.sql; do
filename=$(basename -- "$file")
found=false
for src in "${SRC_DIRS[@]}"; do
if [ -f "$src/$filename" ]; then
found=true
break
fi
done
if [ "$found" = false ]; then
echo "ソースディレクトリに存在しないファイルを削除します。: $file"
rm -f "$file"
if [ $? -ne 0 ]; then
echo "ファイルの削除に失敗しました: $file"
exit 1
fi
fi
done
# allディレクトリのatlas.sumを再計算
atlas migrate hash \
--dir "file://$DEST_DIR"
if [ $? -ne 0 ]; then
echo "atlas.sumの再計算に失敗しました。"
exit 1
fi
echo "マイグレーションファイルの生成に完了しました。"
apply や lint を行う際は、このスクリプトファイルを走らせてから行えば OK です。
Discussion