版本化迁移
快速指南
以下是一些快速步骤,说明如何自动生成并执行针对数据库的迁移文件。欲了解更详细的解释,继续阅读下一节。
生成迁移
To install the latest release of Atlas, simply run one of the following commands in your terminal, or check out the Atlas website:
- macOS + Linux
- Homebrew
- Docker
- Windows
curl -sSf https://atlasgo.sh | sh
brew install ariga/tap/atlas
docker pull arigaio/atlas
docker run --rm arigaio/atlas --help
If the container needs access to the host network or a local directory, use the --net=host flag and mount the desired
directory:
docker run --rm --net=host \
-v $(pwd)/migrations:/migrations \
arigaio/atlas migrate apply
--url "mysql://root:pass@:3306/test"
Download the latest release and move the atlas binary to a file location on your system PATH.
然后,运行以下命令为您的 Ent 模式自动生成迁移文件:
- MySQL
- MariaDB
- PostgreSQL
- SQLite
atlas migrate diff migration_name \
--dir "file://ent/migrate/migrations" \
--to "ent://ent/schema" \
--dev-url "docker://mysql/8/ent"
atlas migrate diff migration_name \
--dir "file://ent/migrate/migrations" \
--to "ent://ent/schema" \
--dev-url "docker://mariadb/latest/test"
atlas migrate diff migration_name \
--dir "file://ent/migrate/migrations" \
--to "ent://ent/schema" \
--dev-url "docker://postgres/15/test?search_path=public"
atlas migrate diff migration_name \
--dir "file://ent/migrate/migrations" \
--to "ent://ent/schema" \
--dev-url "sqlite://file?mode=memory&_fk=1"
Atlas 通过在所提供的开发数据库上执行存储在迁移目录中的 SQL 文件,加载 当前状态。随后,它将该状态与由 ent/schema 包定义的 期望状态 进行比较,并编写一条迁移计划,以从当前状态迁移到期望状态。
应用迁移
要将待处理的迁移文件应用到数据库,运行以下命令:
- MySQL
- MariaDB
- PostgreSQL
- SQLite
atlas migrate apply \
--dir "file://ent/migrate/migrations" \
--url "mysql://root:pass@localhost:3306/example"
atlas migrate apply \
--dir "file://ent/migrate/migrations" \
--url "maria://root:pass@localhost:3306/example"
atlas migrate apply \
--dir "file://ent/migrate/migrations" \
--url "postgres://postgres:pass@localhost:5432/database?search_path=public&sslmode=disable"
atlas migrate apply \
--dir "file://ent/migrate/migrations" \
--url "sqlite://file.db?_fk=1"
欲了解更多信息,请查看Atlas 文档。
迁移状态
使用以下命令获取已连接数据库的迁移状态的详细信息:
- MySQL
- MariaDB
- PostgreSQL
- SQLite
atlas migrate status \
--dir "file://ent/migrate/migrations" \
--url "mysql://root:pass@localhost:3306/example"
atlas migrate status \
--dir "file://ent/migrate/migrations" \
--url "maria://root:pass@localhost:3306/example"
atlas migrate status \
--dir "file://ent/migrate/migrations" \
--url "postgres://postgres:pass@localhost:5432/database?search_path=public&sslmode=disable"
atlas migrate status \
--dir "file://ent/migrate/migrations" \
--url "sqlite://file.db?_fk=1"
深入指南
如果您正在使用 Atlas 迁移引擎,您可以使用版本化迁移工作流。Atlas 并不直接将计算出的变更应用到数据库,而是生成一组包含必要 SQL 语句的迁移文件,用以迁移数据库。随后,您可以根据需求编辑这些文件,并使用许多现有的迁移工具(如 golang-migrate、Flyway 和 Liquibase)来应用它们。
生成版本化迁移文件
迁移文件通过比较两个 状态 的差异来生成。我们将由 Ent 模式所反映的状态称为 期望状态,而 当前状态 是在最近一次更改之前,您模式的最后状态。Ent 有两种方式来确定当前状态:
- 重放现有的迁移目录并检查模式(默认)
- 连接到现有数据库并检查模式
我们强调使用第一种选项,因为它的优点是不必连接到生产数据库来生成差异。另外,这种方法在您有多个不同迁移状态的部署时也同样适用。

为自动生成迁移文件,您可以使用以下两种方式之一:
- 对您的
ent/schema包使用 Atlas 的migrate diff命令。 - 启用
sql/versioned-migration功能标志,并编写一个使用 Atlas 作为包来生成迁移文件的小脚本。
选项 1:使用 atlas migrate diff 命令
- MySQL
- MariaDB
- PostgreSQL
- SQLite
atlas migrate diff migration_name \
--dir "file://ent/migrate/migrations" \
--to "ent://ent/schema" \
--dev-url "docker://mysql/8/ent"
atlas migrate diff migration_name \
--dir "file://ent/migrate/migrations" \
--to "ent://ent/schema" \
--dev-url "docker://mariadb/latest/test"
atlas migrate diff migration_name \
--dir "file://ent/migrate/migrations" \
--to "ent://ent/schema" \
--dev-url "docker://postgres/15/test?search_path=public"
atlas migrate diff migration_name \
--dir "file://ent/migrate/migrations" \
--to "ent://ent/schema" \
--dev-url "sqlite://file?mode=memory&_fk=1"
要在版本化迁移中启用 GlobalUniqueID 选项,请在期望状态后追加查询参数 globalid=1。例如:--to "ent://ent/schema?globalid=1"。
执行上述命令后运行 ls ent/migrate/migrations,您会发现 Atlas 创建了 2 个文件:
- 20220811114629_create_users.sql
- atlas.sum
-- create "users" table
CREATE TABLE `users` (`id` bigint NOT NULL AUTO_INCREMENT, PRIMARY KEY (`id`)) CHARSET utf8mb4 COLLATE utf8mb4_bin;
In addition to the migration directory, Atlas maintains a file name atlas.sum which is used
to ensure the integrity of the migration directory and force developers to deal with situations
where migration order or contents were modified after the fact.
h1:vj6fBSDiLEwe+jGdHQvM2NU8G70lAfXwmI+zkyrxMnk=
20220811114629_create_users.sql h1:wrm4K8GSucW6uMJX7XfmfoVPhyzz3vN5CnU1mam2Y4c=
请前往 应用迁移文件 部分,了解如何将生成的迁移文件应用到数据库。
选项 2:创建迁移生成脚本
第一步是通过传递 sql/versioned-migration 功能标志来启用版本化迁移功能。根据您执行 Ent 代码生成器的方式,您需要使用以下两种选项之一:
- Using Ent CLI
- Using the entc package
如果您使用默认的 go generate 配置,只需在 ent/generate.go 文件中添加 --feature sql/versioned-migration,如下所示:
package ent
//go:generate go run -mod=mod entgo.io/ent/cmd/ent generate --feature sql/versioned-migration ./schema
如果您使用代码生成包(例如,如果您使用 entgql 之类的 Ent 扩展),请按如下方式添加功能标志:
//go:build ignore
package main
import (
"log"
"entgo.io/ent/entc"
"entgo.io/ent/entc/gen"
)
func main() {
err := entc.Generate("./schema", &gen.Config{
Features: []gen.Feature{gen.FeatureVersionedMigration},
})
if err != nil {
log.Fatalf("running ent codegen: %v", err)
}
}
运行 go generate 后,新的创建迁移文件的方法已添加到您的 ent/migrate 包中。接下来的步骤是:
1. 提供一个 Atlas 开发数据库 的 URL 以重放迁移目录并计算 当前 状态。我们使用 docker 来运行本地数据库容器:
- MySQL
- MariaDB
- PostgreSQL
docker run --name migration --rm -p 3306:3306 -e MYSQL_ROOT_PASSWORD=pass -e MYSQL_DATABASE=test -d mysql
docker run --name migration --rm -p 3306:3306 -e MYSQL_ROOT_PASSWORD=pass -e MYSQL_DATABASE=test -d mariadb
docker run --name migration --rm -p 5432:5432 -e POSTGRES_PASSWORD=pass -e POSTGRES_DB=test -d postgres
2. 在 ent/migrate 包下创建名为 main.go 的文件和名为 migrations 的目录,并根据您的项目自定义迁移生成。
- Atlas
- golang-migrate/migrate
- pressly/goose
- amacneil/dbmate
- Flyway
- Liquibase
//go:build ignore
package main
import (
"context"
"log"
"os"
"<project>/ent/migrate"
atlas "ariga.io/atlas/sql/migrate"
"entgo.io/ent/dialect"
"entgo.io/ent/dialect/sql/schema"
_ "github.com/go-sql-driver/mysql"
)
func main() {
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.ModeReplay), // provide migration mode
schema.WithDialect(dialect.MySQL), // Ent dialect to use
schema.WithFormatter(atlas.DefaultFormatter),
}
if len(os.Args) != 2 {
log.Fatalln("migration name is required. Use: 'go run -mod=mod ent/migrate/main.go <name>'")
}
// Generate migrations using Atlas support for MySQL (note the Ent dialect option passed above).
err = migrate.NamedDiff(ctx, "mysql://root:pass@localhost:3306/test", os.Args[1], opts...)
if err != nil {
log.Fatalf("failed generating migration file: %v", err)
}
}
//go:build ignore
package main
import (
"context"
"log"
"os"
"<project>/ent/migrate"
"ariga.io/atlas/sql/sqltool"
"entgo.io/ent/dialect"
"entgo.io/ent/dialect/sql/schema"
_ "github.com/go-sql-driver/mysql"
)
func main() {
ctx := context.Background()
// Create a local migration directory able to understand golang-migrate migration file format for replay.
dir, err := sqltool.NewGolangMigrateDir("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.ModeReplay), // provide migration mode
schema.WithDialect(dialect.MySQL), // Ent dialect to use
}
if len(os.Args) != 2 {
log.Fatalln("migration name is required. Use: 'go run -mod=mod ent/migrate/main.go <name>'")
}
// Generate migrations using Atlas support for MySQL (note the Ent dialect option passed above).
err = migrate.NamedDiff(ctx, "mysql://root:pass@localhost:3306/test", os.Args[1], opts...)
if err != nil {
log.Fatalf("failed generating migration file: %v", err)
}
}
//go:build ignore
package main
import (
"context"
"log"
"os"
"<project>/ent/migrate"
"ariga.io/atlas/sql/sqltool"
"entgo.io/ent/dialect"
"entgo.io/ent/dialect/sql/schema"
_ "github.com/go-sql-driver/mysql"
)
func main() {
ctx := context.Background()
// Create a local migration directory able to understand goose migration file format for replay.
dir, err := sqltool.NewGooseDir("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.ModeReplay), // provide migration mode
schema.WithDialect(dialect.MySQL), // Ent dialect to use
}
if len(os.Args) != 2 {
log.Fatalln("migration name is required. Use: 'go run -mod=mod ent/migrate/main.go <name>'")
}
// Generate migrations using Atlas support for MySQL (note the Ent dialect option passed above).
err = migrate.NamedDiff(ctx, "mysql://root:pass@localhost:3306/test", os.Args[1], opts...)
if err != nil {
log.Fatalf("failed generating migration file: %v", err)
}
}
//go:build ignore
package main
import (
"context"
"log"
"os"
"<project>/ent/migrate"
"ariga.io/atlas/sql/sqltool"
"entgo.io/ent/dialect"
"entgo.io/ent/dialect/sql/schema"
_ "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("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.ModeReplay), // provide migration mode
schema.WithDialect(dialect.MySQL), // Ent dialect to use
}
if len(os.Args) != 2 {
log.Fatalln("migration name is required. Use: 'go run -mod=mod ent/migrate/main.go <name>'")
}
// Generate migrations using Atlas support for MySQL (note the Ent dialect option passed above).
err = migrate.NamedDiff(ctx, "mysql://root:pass@localhost:3306/test", os.Args[1], opts...)
if err != nil {
log.Fatalf("failed generating migration file: %v", err)
}
}
//go:build ignore
package main
import (
"context"
"log"
"os"
"<project>/ent/migrate"
"ariga.io/atlas/sql/sqltool"
"entgo.io/ent/dialect"
"entgo.io/ent/dialect/sql/schema"
_ "github.com/go-sql-driver/mysql"
)
func main() {
ctx := context.Background()
// Create a local migration directory able to understand Flyway migration file format for replay.
dir, err := sqltool.NewFlywayDir("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.ModeReplay), // provide migration mode
schema.WithDialect(dialect.MySQL), // Ent dialect to use
}
if len(os.Args) != 2 {
log.Fatalln("migration name is required. Use: 'go run -mod=mod ent/migrate/main.go <name>'")
}
// Generate migrations using Atlas support for MySQL (note the Ent dialect option passed above).
err = migrate.NamedDiff(ctx, "mysql://root:pass@localhost:3306/test", os.Args[1], opts...)
if err != nil {
log.Fatalf("failed generating migration file: %v", err)
}
}
//go:build ignore
package main
import (
"context"
"log"
"os"
"<project>/ent/migrate"
"ariga.io/atlas/sql/sqltool"
"entgo.io/ent/dialect"
"entgo.io/ent/dialect/sql/schema"
_ "github.com/go-sql-driver/mysql"
)
func main() {
ctx := context.Background()
// Create a local migration directory able to understand Liquibase migration file format for replay.
dir, err := sqltool.NewLiquibaseDir("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.ModeReplay), // provide migration mode
schema.WithDialect(dialect.MySQL), // Ent dialect to use
}
if len(os.Args) != 2 {
log.Fatalln("migration name is required. Use: 'go run -mod=mod ent/migrate/main.go <name>'")
}
// Generate migrations using Atlas support for MySQL (note the Ent dialect option passed above).
err = migrate.NamedDiff(ctx, "mysql://root:pass@localhost:3306/test", os.Args[1], opts...)
if err != nil {
log.Fatalf("failed generating migration file: %v", err)
}
}
3. 通过在项目根目录执行 go run -mod=mod ent/migrate/main.go <name> 启动迁移生成。例如:
go run -mod=mod ent/migrate/main.go create_users
执行上述命令后运行 ls ent/migrate/migrations,您会发现 Atlas 创建了 2 个文件:
- 20220811114629_create_users.sql
- atlas.sum
-- create "users" table
CREATE TABLE `users` (`id` bigint NOT NULL AUTO_INCREMENT, PRIMARY KEY (`id`)) CHARSET utf8mb4 COLLATE utf8mb4_bin;
In addition to the migration directory, Atlas maintains a file name atlas.sum which is used
to ensure the integrity of the migration directory and force developers to deal with situations
where migration order or contents were modified after the fact.
h1:vj6fBSDiLEwe+jGdHQvM2NU8G70lAfXwmI+zkyrxMnk=
20220811114629_create_users.sql h1:wrm4K8GSucW6uMJX7XfmfoVPhyzz3vN5CnU1mam2Y4c=
完整的参考示例可在 GitHub 仓库 中找到。
验证和检查迁移
生成迁移文件后,我们可以运行 atlas migrate lint 命令,对迁移目录的内容进行验证、分析并生成洞察与诊断:
- 确保迁移历史可以从任何时间点回放。
- 防止在多个团队成员并发写入迁移目录时出现意外的历史变更。了解一致性检查可参见下方的Atlas 迁移目录完整性文件章节。
- 检测是否已进行 破坏性(destructive)或不可逆变更,或这些变更是否依赖于表的内容并可能导致迁移失败。
运行如下命令以执行迁移检查:
--dev-url指向将用于重放变更的 开发数据库 的 URL。--dir指向迁移目录的 URL,默认是file://migrations。--dir-format自定义目录格式,默认是atlas。- (可选)
--log使用 Go 模板自定义日志。 - (可选)
--latest对最近N个迁移文件进行分析。 - (可选)
--git-base对比基底 Git 分支进行分析。
安装 Atlas:
To install the latest release of Atlas, simply run one of the following commands in your terminal, or check out the Atlas website:
- macOS + Linux
- Homebrew
- Docker
- Windows
curl -sSf https://atlasgo.sh | sh
brew install ariga/tap/atlas
docker pull arigaio/atlas
docker run --rm arigaio/atlas --help
If the container needs access to the host network or a local directory, use the --net=host flag and mount the desired
directory:
docker run --rm --net=host \
-v $(pwd)/migrations:/migrations \
arigaio/atlas migrate apply
--url "mysql://root:pass@:3306/test"
Download the latest release and move the atlas binary to a file location on your system PATH.
运行 atlas migrate lint 命令:
- MySQL
- MariaDB
- PostgreSQL
- SQLite
atlas migrate lint \
--dev-url="docker://mysql/8/test" \
--dir="file://ent/migrate/migrations" \
--latest=1
atlas migrate lint \
--dev-url="docker://mariadb/latest/test" \
--dir="file://ent/migrate/migrations" \
--latest=1
atlas migrate lint \
--dev-url="docker://postgres/15/test?search_path=public" \
--dir="file://ent/migrate/migrations" \
--latest=1
atlas migrate lint \
--dev-url="sqlite://file?mode=memory" \
--dir="file://ent/migrate/migrations" \
--latest=1
此类运行的输出可能如下所示:
20221114090322_add_age.sql: data dependent changes detected:
L2: Adding a non-nullable "double" column "age" on table "users" without a default value implicitly sets existing rows with 0
20221114101516_add_name.sql: data dependent changes detected:
L2: Adding a non-nullable "varchar" column "name" on table "users" without a default value implicitly sets existing rows with ""
关于全局唯一 ID 的说明
本节仅适用于使用 全局唯一 ID 功能的 MySQL 用户。
使用全局唯一 ID 时,Ent 为每个表分配 1<<32 个整数值的范围。这通过为第一张表设定自增起始值为 1,第二张表设定为 4294967296,第三张表设定为 8589934592,依此类推来实现。表获得起始值的顺序被保存在名为 ent_types 的额外表中。对于 MySQL 5.6 与 5.7,自动递增起始值只保存在内存中(文档,InnoDB AUTO_INCREMENT Counter Initialization 标题),并在启动时通过查看任何表的最后一个插入 ID 来重新计算。现在,如果您恰好有一张还没有行的表,自动递增起始值将为每张没有条目的表设置为 0。在线迁移功能下,这不是问题,因为迁移引擎会查看 ent_types 表并确保如果计数器未正确设置就更新它。然而,使用版本化迁移时则不再如此。为确保在服务器重启后一切设置正确,请确保调用 Atlas 结构体上的 VerifyTableRange 方法:
package main
import (
"context"
"log"
"<project>/ent"
"<project>/ent/migrate"
"entgo.io/ent/dialect/sql"
"entgo.io/ent/dialect/sql/schema"
_ "github.com/go-sql-driver/mysql"
)
func main() {
drv, err := sql.Open("mysql", "user:pass@tcp(localhost:3306)/ent")
if err != nil {
log.Fatalf("failed opening connection to mysql: %v", err)
}
defer drv.Close()
// Verify the type allocation range.
m, err := schema.NewMigrate(drv, nil)
if err != nil {
log.Fatalf("failed creating migrate: %v", err)
}
if err := m.VerifyTableRange(context.Background(), migrate.Tables); err != nil {
log.Fatalf("failed verifyint range allocations: %v", err)
}
client := ent.NewClient(ent.Driver(drv))
// ... do stuff with the client
}
升级到 MySQL 8 时,您仍然需要运行该方法一次以更新起始值。自 MySQL 8 起,计数器不再仅保存在内存中,这意味着在第一次调用后后续调用不再需要。
应用迁移文件
Ent 建议使用 Atlas CLI 将生成的迁移文件应用到数据库。如果您想使用其他迁移管理工具,Ent 已经支持为其中的几个工具生成迁移文件。
- MySQL
- MariaDB
- PostgreSQL
- SQLite
atlas migrate apply \
--dir "file://ent/migrate/migrations" \
--url "mysql://root:pass@localhost:3306/example"
atlas migrate apply \
--dir "file://ent/migrate/migrations" \
--url "maria://root:pass@localhost:3306/example"
atlas migrate apply \
--dir "file://ent/migrate/migrations" \
--url "postgres://postgres:pass@localhost:5432/database?search_path=public&sslmode=disable"
atlas migrate apply \
--dir "file://ent/migrate/migrations" \
--url "sqlite://file.db?_fk=1"
欲了解更多信息,请查看Atlas 文档。
在旧版本的 Ent 中,golang-migrate/migrate 是默认的迁移执行引擎。为便于迁移,Atlas 可以为您导入 golang-migrate 的迁移格式。您可以在Atlas 文档中了解更多信息。
从自动迁移迁移到版本化迁移
如果您已经在生产环境中拥有一个 Ent 应用程序,并希望从自动迁移切换到新的版本化迁移,您需要执行一些额外步骤。
创建反映当前已部署状态的初始迁移文件
为此,请确保您的模式定义与已部署的版本同步。然后启动一个空数据库,并按上述方式运行一次 diff 命令。这将创建创建您模式图当前状态所需的语句。如果您在之前启用了 通用 ID,每次部署都会有一个名为 ent_types 的特殊数据库表。上述命令将生成创建该表及其内容所需的 SQL 语句(类似于下例):
CREATE TABLE `users` (`id` integer NOT NULL PRIMARY KEY AUTOINCREMENT);
CREATE TABLE `groups` (`id` integer NOT NULL PRIMARY KEY AUTOINCREMENT);
INSERT INTO sqlite_sequence (name, seq) VALUES ("groups", 4294967296);
CREATE TABLE `ent_types` (`id` integer NOT NULL PRIMARY KEY AUTOINCREMENT, `type` text NOT NULL);
CREATE UNIQUE INDEX `ent_types_type_key` ON `ent_types` (`type`);
INSERT INTO `ent_types` (`type`) VALUES ('users'), ('groups');
为避免破坏现有代码,请确保该文件的内容与您从中生成 diff 的数据库表中的内容相同。例如,如果您考虑上例的迁移文件(users,groups),但您的已部署表看起来像下表(groups,users):
| id | type |
|---|---|
| 1 | groups |
| 2 | users |
您可以看到顺序不同。在这种情况下,您必须手动更改生成的迁移文件中的两个条目。
使用 Atlas 基线迁移
如果您将 Atlas 作为迁移执行引擎,则可以简单地使用 --baseline 标志。对于其他工具,请参阅各自的文档。
atlas migrate apply \
--dir "file://migrations"
--url mysql://root:pass@localhost:3306/ent
--baseline "<version>"
Atlas 迁移目录完整性文件
问题陈述
假设您有多个团队并行开发一个功能,并且两者都需要迁移。如果 Team A 与 Team B 未相互检查,则他们可能最终得到一组损坏的迁移文件(例如,添加相同的表或列两次),因为新文件不会在 Git 等版本控制系统中产生合并冲突。以下示例演示了此类行为:
假设 Team A 与 Team B 都在各自的分支上添加了一个名为 User 的新架构并生成了版本化迁移文件。
-- create "users" table
CREATE TABLE `users` (
`id` bigint NOT NULL AUTO_INCREMENT,
`team_a_col` INTEGER NOT NULL,
PRIMARY KEY (`id`)
) CHARSET utf8mb4 COLLATE utf8mb4_bin;
-- create "users" table
CREATE TABLE `users` (
`id` bigint NOT NULL AUTO_INCREMENT,
`team_b_col` INTEGER NOT NULL,
PRIMARY KEY (`id`)
) CHARSET utf8mb4 COLLATE utf8mb4_bin;
当他们都将分支合并到主分支时,Git 不会产生冲突,一切看似正常。但尝试执行待处理迁移将导致迁移失败:
mysql> CREATE TABLE `users` (`id` bigint NOT NULL AUTO_INCREMENT, `team_a_col` INTEGER NOT NULL, PRIMARY KEY (`id`)) CHARSET utf8mb4 COLLATE utf8mb4_bin;
[2022-04-14 10:00:38] completed in 31 ms
mysql> CREATE TABLE `users` (`id` bigint NOT NULL AUTO_INCREMENT, `team_b_col` INTEGER NOT NULL, PRIMARY KEY (`id`)) CHARSET utf8mb4 COLLATE utf8mb4_bin;
[2022-04-14 10:00:48] [42S01][1050] Table 'users' already exists
根据 SQL 的不同,可能会使数据库处于受损状态。
解决方案
幸运的是,Atlas 迁移引擎提供了一种方式来防止并发创建新迁移文件并保护迁移历史的完整性,我们称之为 迁移目录完整性文件,它只是您的迁移目录中的另一个名为 atlas.sum 的文件。对于 Team A 的迁移目录,它可能如下所示:
h1:KRFsSi68ZOarsQAJZ1mfSiMSkIOZlMq4RzyF//Pwf8A=
20220318104614_team_A.sql h1:EGknG5Y6GQYrc4W8e/r3S61Aqx2p+NmQyVz/2m8ZNwA=
atlas.sum 文件包含每个迁移文件的校验和(通过反向单分支 Merkle 哈希树实现),以及所有文件的总和。添加新文件会导致总和文件发生变化,从而在大多数版本控制系统中产生合并冲突。让我们看看如何使用 迁移目录完整性文件 自动检测上述情况。
请注意,您需要在系统中安装 Atlas CLI,才能让此功能正常工作,请确保在继续之前遵循安装说明。
在旧版本的 Ent 中,完整性文件是可选的。但我们认为这是一个非常重要的功能,为迁移提供了巨大的价值和安全性。因此,生成 sum 文件现在已成为默认行为,并且将来我们甚至可能取消禁用此功能的选项。现在,如果您真的想删除完整性文件生成,请使用 schema.DisableChecksum() 选项。
除了常规的 .sql 迁移文件之外,迁移目录将包含 atlas.sum 文件。每次让 Ent 生成新的迁移文件时,该文件都会为您更新。然而,每次手动更改迁移目录都会导致迁移目录和 atlas.sum 文件不同步。使用 Atlas CLI,您可以检查文件与迁移目录是否同步,并在不同步时修复:
# 如果没有输出,表示迁移目录是同步的。
atlas migrate validate --dir file://<path-to-your-migration-directory>
# 如果迁移目录和 sum 文件不同步,Atlas CLI 会提示您。
atlas migrate validate --dir file://<path-to-your-migration-directory>
Error: checksum mismatch
You have a checksum error in your migration directory.
This happens if you manually create or edit a migration file.
Please check your migration files and run
'atlas migrate hash'
to re-hash the contents and resolve the error.
exit status 1
如果您确定迁移文件中的内容正确,可以在 atlas.sum 文件中重新计算哈希:
# Recompute the sum file.
atlas migrate hash --dir file://<path-to-your-migration-directory>
回到上面的问题,如果 Team A 首先将其更改合并到主分支,并且 Team B 现在尝试合并自己的更改,他们将产生合并冲突,正如下图所示:
您可以将 atlas migrate validate 调用添加到 CI,持续检查迁移目录。即使任何团队成员忘记在手动编辑后更新 atlas.sum 文件,CI 也不会成功,表明存在问题。