跳到主要内容

版本化迁移管理与迁移目录完整性

· 阅读需 11 分钟

五周前我们发布了 Ent 中备受期待的功能——版本化迁移:用于管理数据库变更。
公告博客中,我们简要介绍了声明式和基于变更的方法,以保持数据库模式与正在使用的应用程序同步,并讨论了它们的缺陷以及 Atlas'(Ent 基础迁移引擎)将两者最佳特性结合到单一工作流中为何值得一试。我们称之为 Versioned Migration Authoring,如果你还没读过,现在正是时候!

使用版本化迁移作者时,生成的迁移文件仍然是“基于变更”的,但已被 Atlas 引擎安全规划。这意味着你仍然可以使用你最喜欢的迁移管理工具,例如 FlywayLiquibasegolang-migrate/migratepressly/goose,在使用 Ent 开发服务时。

在本文中,我想向你展示 Atlas 项目中的另一个新功能——我们称之为 Migration Directory Integrity File(迁移目录完整性文件),它现在已在 Ent 中得到支持,并且你可以与任何你已习惯且喜爱的迁移管理工具一起使用。

问题

在使用版本化迁移时,开发人员需要注意以下事项,以免破坏数据库:

  1. 事后更改已执行的迁移。
  2. 不小心更改迁移的组织顺序。
  3. 检入语义不正确的 SQL 脚本。

理论上,代码审查应该能防止团队合并带有这些问题的迁移。根据我的经验,事实上很多错误会通过人眼漏检,使这一方法容易出错。因此,采用自动化方式防止这些错误更安全。

第一个问题(更改历史)被大多数管理工具通过在托管数据库中保存已执行迁移文件的哈希值并与文件进行比较来解决。如果它们不匹配,迁移就会被中止。然而,这在开发周期的非常后期(部署期间)发生,如果能更早检测到可能会节省时间和资源。

第二个(和第三个)问题,考虑以下情景:

atlas-versioned-migrations-no-conflict

此图展示了两种可能被忽视的错误。第一个是迁移文件的顺序。

Team A 和 Team B 大致在同一时间分支一个功能。Team B 生成一个版本时间戳为 x 的迁移文件并继续开发。Team A 在稍后某个时间点生成迁移文件,因此版本时间戳为 x+1。Team A 完成了功能并将其合并到主分支,可能会自动在生产环境中部署,并将迁移版本 x+1 申请。此时没有问题。

现在,Team B 用版本 x 的迁移文件合并它的功能,该迁移时间戳早于已应用的 x+1。如果代码审查流程没有检测到,它将迁移文件推送到生产,随后就取决于具体的迁移管理工具来决定接下来会发生什么。

大多数工具都有自己的解决方案,例如 pressly/goose 采用了我们称之为 hybrid versioning 的方法(详见 https://github.com/pressly/goose/issues/63#issuecomment-428681694)。在我向你介绍 Atlas(Ent)处理此问题的独特方式之前,让我们先快速看一下第三个问题:

如果 Team A 和 Team B 开发的功能都需要新表或列,并且它们给了相同的名称(例如 users),它们可能都生成创建该表的语句。虽然先合并的团队将成功迁移,但后合并的团队迁移会因表或列已存在而失败。

解决方案

Atlas 以独特的方式处理上述问题。我们的目标是在尽可能早的时间提高意识。我们认为最佳做法是在版本控制和持续集成(CI)阶段。Atlas 的解决方案是引入一种新文件——Migration Directory Integrity File(迁移目录完整性文件)。它只是另一个名为 atlas.sum 的文件,存放在迁移文件旁边,包含关于迁移目录的一些元数据。其格式受到 Go 模块 go.sum 文件的启发,看起来类似于:

h1:KRFsSi68ZOarsQAJZ1mfSiMSkIOZlMq4RzyF//Pwf8A=
20220318104614_team_A.sql h1:EGknG5Y6GQYrc4W8e/r3S61Aqx2p+NmQyVz/2m8ZNwA=

atlas.sum 文件的第一个条目包含整个目录的总和,随后为每个迁移文件提供一个校验和(实现方式是逆向单分支 Merkle 哈希树)。让我们看看如何使用此文件在版本控制和 CI 中检测之前提到的情况。我们的目标是让两支团队都意识到已经添加了迁移文件,并且它们很可能需要在继续合并前进行检查。

备注

要跟随演示,请执行以下命令快速创建一个示例项目。这些命令将完成:

  1. 创建 Go 模块并下载所有依赖
  2. 创建一个非常简单的 User 模式
  3. 启用版本化迁移功能
  4. 运行代码生成
  5. 启动一个 MySQL Docker 容器(使用 docker stop atlas-sum 停止)
mkdir ent-sum-file
cd ent-sum-file
go mod init ent-sum-file
go install entgo.io/ent/cmd/ent@master
go run entgo.io/ent/cmd/ent new User
sed -i -E 's|^//go(.*)$|//go\1 --feature sql/versioned-migration|' ent/generate.go
go generate ./...
docker run --rm --name atlas-sum --detach --env MYSQL_ROOT_PASSWORD=pass --env MYSQL_DATABASE=ent -p 3306:3306 mysql

第一步是告诉迁移引擎使用 schema.WithSumFile() 选项来创建并管理 atlas.sum。下面的示例使用一个已实例化的 Ent 客户端 /docs/versioned-migrations#from-client 生成新的迁移文件:

package main

import (
"context"
"log"
"os"

"ent-sum-file/ent"

"ariga.io/atlas/sql/migrate"
"entgo.io/ent/dialect/sql/schema"
_ "github.com/go-sql-driver/mysql"
)

func main() {
client, err := ent.Open("mysql", "root:pass@tcp(localhost:3306)/ent")
if err != nil {
log.Fatalf("failed connecting to mysql: %v", err)
}
defer client.Close()
ctx := context.Background()
// Create a local migration directory.
dir, err := migrate.NewLocalDir("migrations")
if err != nil {
log.Fatalf("failed creating atlas migration directory: %v", err)
}
// Write migration diff.
err = client.Schema.NamedDiff(ctx, os.Args[1], schema.WithDir(dir), schema.WithSumFile())
if err != nil {
log.Fatalf("failed creating schema resources: %v", err)
}
}

创建迁移目录并执行上述命令后,你应该会看到兼容 golang-migrate/migrate 的迁移文件,并且还会生成 atlas.sum 文件,内容如下:

mkdir migrations
go run -mod=mod main.go initial
20220504114411_initial.up.sql
-- create "users" table
CREATE TABLE `users` (`id` bigint NOT NULL AUTO_INCREMENT, PRIMARY KEY (`id`)) CHARSET utf8mb4 COLLATE utf8mb4_bin;

20220504114411_initial.down.sql
-- reverse: create "users" table
DROP TABLE `users`;

atlas.sum
h1:SxbWjP6gufiBpBjOVtFXgXy7q3pq1X11XYUxvT4ErxM=
20220504114411_initial.down.sql h1:OllnelRaqecTrPbd2YpDbBEymCpY/l6ihbyd/tVDgeY=
20220504114411_initial.up.sql h1:o/6yOczGSNYQLlvALEU9lK2/L6/ws65FrHJkEk/tjBk=

如你所见,atlas.sum 文件为每个生成的迁移文件包含一条条目。启用 atlas.sum 生成后,Team A 与 Team B 在为模式变更生成迁移时都会拥有这样一个文件。版本控制在第二个团队尝试合并其功能时会触发合并冲突。

atlas-versioned-migrations-no-conflict

备注

在随后的步骤中,我们使用 go run -mod=mod ariga.io/atlas/cmd/atlas 调用 Atlas CLI,但你也可以全局安装 CLI(随后直接使用 atlas)按以下安装说明进行配置:https://atlasgo.io/cli/getting-started/setting-up#install-the-cli。

你可以随时检查 atlas.sum 文件是否与迁移目录保持同步:

go run -mod=mod ariga.io/atlas/cmd/atlas migrate validate

然而,如果你手动更改了迁移文件(例如添加新的 SQL 语句、编辑现有语句甚至创建全新文件),atlas.sum 文件将不再与迁移目录内容保持同步。尝试为模式变更生成新的迁移文件时,Atlas 迁移引擎会被阻止。试试创建一个空的迁移文件并再次运行 main.go

go run -mod=mod ariga.io/atlas/cmd/atlas migrate new migrations/manual_version.sql --format golang-migrate
go run -mod=mod main.go initial
# 2022/05/04 15:08:09 failed creating schema resources: validating migration directory: checksum mismatch
# exit status 1

atlas migrate validate 命令会给出相同的提示:

go run -mod=mod ariga.io/atlas/cmd/atlas migrate validate
# 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 --force'
#
# to re-hash the contents and resolve the error.
#
# exit status 1

若想让 atlas.sum 文件再次与迁移目录同步,我们可以再次使用 Atlas CLI:

go run -mod=mod ariga.io/atlas/cmd/atlas migrate hash --force

出于安全原因,Atlas CLI 不会在 atlas.sum 文件不同步的目录上执行操作。因此,你需要在命令中加入 --force 标志。

对于开发者忘记在手动更改后更新 atlas.sum 文件的情况,你可以在 CI 中添加 atlas migrate validate 调用。我们正在积极开发 GitHub Action 和 CI 解决方案,能够 开箱即用 为你完成(以及其他)这一步骤。

小结

本文简要介绍了使用基于变更的 SQL 文件时常见的模式迁移来源,并基于 Atlas 项目提供的解决方案,使迁移更加安全。

有问题吗?需要帮助入门吗?随时加入我们的 Ent Discord 服务器

更多 Ent 新闻与更新