版本化迁移
快速指南
以下是快速生成并执行数据库迁移文件的几个步骤。如需更深入的说明,请继续阅读下一节。 以下是几个快速步骤来解释如何自动生成并执行数据库迁移文件。更深入的解释请继续阅读 下一章节。
生成迁移
要安装Atlas的最新版本,只需在终端中运行以下任一命令,或访问Atlas 官方网站:
- 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
如果容器需要访问主机网络或本地目录,请使用 --net=host 标志挂载所需目录:
docker run --rm --net=host \
-v $(pwd)/migrations:/migrations \
arigaio/atlas migrate apply
--url "mysql://root:pass@:3306/test"
下载 最新版本 并将 atlas 二进制执行文件所在目录加入到系统路径中。
如何运行下面的命令来为 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-migrat、Flyway 或 Liquibase。
生成版本化迁移文件
迁移文件通过计算两种 状态(states) 之间的不同进行生成。 我们将 Ent 模式的状态映射称之为 (期望)desired 状态,将你在最近修改之前的最终模式的状态称为 当前 状态。 在 Ent 中当前状态有两种方式决定:
- 重新加载现有的迁移目录并检查模式(默认)
- 连接到已有数据库并检查模式
我们强调应优先采用第一种方案,其优势在于无需连接生产数据库即可生成差异文件。 此外,当存在多个处于不同迁移状态的部署时,此方法同样适用。

为自动生成迁移文件,你可以采用以下两种方式之一:
- 在
ent/schema包中使用 Atlasmigrate 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 创建了两个文件:
- 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;
除了迁移目录外,Atlas 还维护一个名为 atlas.sum 的文件,用于确保迁移目录的完整性,并强制开发人员处理迁移顺序或事后内容被修改的问题。
h1:vj6fBSDiLEwe+jGdHQvM2NU8G70lAfXwmI+zkyrxMnk=
20220811114629_create_users.sql h1:wrm4K8GSucW6uMJX7XfmfoVPhyzz3vN5CnU1mam2Y4c=
前往 应用迁移文件 章节了解如何执行生成的迁移文件到数据库。
Option 2: 创建迁移生成脚本
第一步是传入 sql/versioned-migration 功能标志来启动版本化迁移功能。
取决于你如何执行 Ent 代码生成,可以使用以下两种方式之一:
- Using Ent CLI
- Using the entc package
如果你使用默认的 go generate 配置,只需要简单地添加 --feature sql/versioned-migration 到 ent/generate.go 文件,如下:
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. 创建一个名为 main.go 的文件和一个在 ent/migrate 包下名为 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 创建了两个文件:
- 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;
除了迁移目录外,Atlas 还维护一个名为 atlas.sum 的文件,用于确保迁移目录的完整性,并强制开发人员处理迁移顺序或事后内容被修改的问题。
h1:vj6fBSDiLEwe+jGdHQvM2NU8G70lAfXwmI+zkyrxMnk=
20220811114629_create_users.sql h1:wrm4K8GSucW6uMJX7XfmfoVPhyzz3vN5CnU1mam2Y4c=
完整参考示例参见 GitHub 仓库。
验证和检查迁移
在使用 Atlas 生成迁移文件后,可以运行 atlas migrate lint 命令来验证并分析迁移目录,
并对选定的变更生成洞察和诊断信息:
- 确认迁移历史在任何时间点都可以被重载。
- 防止多个团队成员写入迁移目录时的意外历史变更和迁移冲突。更多关于一致性检查的详情,请参阅 下文章节。
- 检测是否已发生 破坏性的 或不可逆的变更,或这些变更是否依赖于表内容,从而可能导致迁移失败。
让我们带有必要的参数运行 atlas migrate lint 来进行迁移检查:
--dev-url用来重载变更的 开发数据库 的 URL 。--dirURL 指向的迁移目录,默认file://migrations。--dir-format自定义目录格式,默认atlas。- (optional)
--log使用 Go 语言模板的自定义日志。 - (optional)
--latest在最近的N个迁移文件上执行分析。 - (optional)
--git-base在基础的 Git 分支上执行分析。
安装 Atlas:
要安装Atlas的最新版本,只需在终端中运行以下任一命令,或访问Atlas 官方网站:
- 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
如果容器需要访问主机网络或本地目录,请使用 --net=host 标志挂载所需目录:
docker run --rm --net=host \
-v $(pwd)/migrations:/migrations \
arigaio/atlas migrate apply
--url "mysql://root:pass@:3306/test"
下载 最新版本 并将 atlas 二进制执行文件所在目录加入到系统路径中。
运行 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 用户
使用全局唯一标识符时,Ent 为每个表分配一个 1<<32 的整数值范围。
具体实现方式是:将第一个表的自动递增起始值设为 1,第二个表起始值设为 4294967296,第三个表设为 8589934592,依此类推。
各表接收起始值的顺序会被保存到名为 ent_types 的额外表中。
在 MySQL 5.6 和 5.7 版本中,自动递增起始值仅保存在内存中(参见:文档 InnoDB AUTO_INCREMENT 计数器初始化 头部),
系统启动时会根据每个表中最后插入的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');
为确保不破坏现有代码,请确认该文件内容与你创建差异比较的数据库中表的内容相同。
例如,若你考虑上述迁移文件(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 迁移目录完整性文件
问题
假设有多个团队并行开发同一功能且都需要迁移操作。 如果团队 A 和团队 B 之间不相互检查,最终可能会导致迁移文件出现冲突(例如重复添加相同的表或列),因为在 Git 等版本控制系统中新增文件不会引起合并冲突。 以下示例展示了这种情况:
假设团队 A 和团队 B 都在各自的分支上添加了一个名为 User 的新模式并生成版本化迁移文件。
-- create "users" table
CREATE TABLE `users` (
`id` bigint NOT NULL AUTO_INCREMENT,
// highlight-start
`team_a_col` INTEGER NOT NULL,
// highlight-end
PRIMARY KEY (`id`)
) CHARSET utf8mb4 COLLATE utf8mb4_bin;
-- create "users" table
CREATE TABLE `users` (
`id` bigint NOT NULL AUTO_INCREMENT,
// highlight-start
`team_b_col` INTEGER NOT NULL,
// highlight-end
PRIMARY KEY (`id`)
) CHARSET utf8mb4 COLLATE utf8mb4_bin;
如果他们都合并自己的分支到 master(主分支),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 的。以团队 A 的迁移目录为例,其内容大致如下所示:
h1:KRFsSi68ZOarsQAJZ1mfSiMSkIOZlMq4RzyF//Pwf8A=
20220318104614_team_A.sql h1:EGknG5Y6GQYrc4W8e/r3S61Aqx2p+NmQyVz/2m8ZNwA=
atlas.sum 文件包含每个迁移文件(通过反向单分支默克尔哈希树实现 - merkle tree 实现)的校验总结和所有文件的总结。
添加新文件会导致变更总结文件,在多数版本控制系统中引起合并冲突。让我们看一下如何使用 迁移目录完整性文件 来自动化检测上面的场景:
请注意这一步你需要在系统中安装 Atlas CLI,在进一步之前确保遵从 安装介绍。
在 Ent 的早期版本中,完整性文件是可选功能。但我们认为这是极具价值且能为迁移提供安全保障的重要特性。
因此,现在默认会生成总结和文件,未来甚至可能移除禁用此功能的选项。若您确实需要禁用完整性文件生成,目前可通过 schema.DisableChecksum() 选项实现。
除了常规的 .sql 迁移文件外,迁移目录还将包含 atlas.sum 文件。每次让 Ent 生成新的迁移文件时,该文件都会自动更新。
然而,对迁移目录进行的任何手动修改都会导致迁移目录与 atlas.sum 文件出现不一致。通过 Atlas CLI,您可以检查文件与迁移目录是否同步,并在不一致时进行修复:
# If there is no output, the migration directory is in-sync.
atlas migrate validate --dir file://<path-to-your-migration-directory>
# If the migration directory and sum file are out-of-sync the Atlas CLI will tell you.
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>
回到上述问题,如果团队 A 团队先将变更提交到主分支,而团队 B 此时尝试提交自己的变更,就会出现合并冲突,如下例所示:
您可以在持续集成中添加 atlas migrate validate 调用以实现对迁移目录的持续检查。
即使团队成员在手动编辑后忘记更新 atlas.sum 文件,持续集成也不会显示为绿色,从而提示存在问题。