当 Ariel 在一月底发布了 Ent v0.10.0 时,他 引入 了一个基于另一个开源项目 Atlas 的新的迁移引擎。
Initially, Atlas支持一种我们称之为“声明式迁移”的数据库模式管理方式。使用声明式迁移时,数据库模式的期望状态作为输入提供给迁移引擎,迁移引擎随后规划并执行一系列操作将数据库变更为期望状态。这一方法已被云原生基础设施领域的项目(如 Kubernetes 和 Terraform)推广。它在许多情况下效果良好,事实上在过去几年里对 Ent 框架也做得非常好。然而,数据库迁移是一个非常敏感的话题,许多项目需要更受控的方式。
因此,大多数行业标准解决方案,如 Flyway,Liquibase,或 golang-migrate/migrate(在 Go 生态系统中很常见),都支持它们称为“版本化迁移”的工作流。
使用版本化迁移(有时也称为“基于变更”的迁移),你不再描述期望状态(“数据库应该是什么样子”),而是描述变更本身(“如何达到该状态”)。大多数情况通过创建一组包含所需语句的 SQL 文件来完成。每个文件被分配一个唯一的版本和描述变更的说明。上述工具随后能够解释迁移文件并在正确的顺序中应用(部分)它们,以过渡到期望的数据库结构。
在本帖中,我想展示一种最近添加到 Atlas 和 Ent 的新型迁移工作流。我们称之为“版本化迁移编写”,它尝试将声明式方法的简洁与表达力与版本化迁移的安全和明确性相结合。使用版本化迁移编写,用户仍然会声明期望状态,并使用 Atlas 引擎规划一条从现有状态到新状态的安全迁移。然而,而不是将规划与执行耦合在一起,这一步骤会被写入文件,文件可以提交到源控制、手动微调并在正常的代码评审流程中审查。
举例来说,我将使用 golang-migrate/migrate 演示该工作流。
开始使用
第一件事是确保你拥有最新的 Ent 版本:
go get -u entgo.io/ent@master
Ent 生成模式变更迁移文件有两种方式。第一种是使用实例化的 Ent 客户端,第二种则是根据解析后的模式图生成变更。本篇文章将采用第二种方法,如果你想了解第一种方法,请查看 文档。
生成版本化迁移文件
既然我们已经启用了版本化迁移功能,让我们创建一个小的模式并生成初始的迁移文件集合。以下是新建 Ent 项目的示例模式:
package schema
import (
"entgo.io/ent"
"entgo.io/ent/schema/field"
"entgo.io/ent/schema/index"
)
// User holds the schema definition for the User entity.
type User struct {
ent.Schema
}
// Fields of the User.
func (User) Fields() []ent.Field {
return []ent.Field{
field.String("username"),
}
}
// Indexes of the User.
func (User) Indexes() []ent.Index {
return []ent.Index{
index.Fields("username").Unique(),
}
}
正如我之前提到的,我们想使用解析后的模式图来计算我们的模式与已连接数据库之间的差异。以下是一个(半)持久化的 MySQL Docker 容器示例,如果你想跟着操作可以使用:
docker run --rm --name ent-versioned-migrations --detach --env MYSQL_ROOT_PASSWORD=pass --env MYSQL_DATABASE=ent -p 3306:3306 mysql
完成后,你可以通过 docker stop ent-versioned-migrations 停止并移除所有资源。
现在,让我们创建一个小函数来加载模式图并生成迁移文件。创建一个名为 main.go 的 Go 文件,并复制以下内容:
package main
import (
"context"
"log"
"os"
"ariga.io/atlas/sql/migrate"
"entgo.io/ent/dialect/sql"
"entgo.io/ent/dialect/sql/schema"
"entgo.io/ent/entc"
"entgo.io/ent/entc/gen"
_ "github.com/go-sql-driver/mysql"
)
func main() {
// We need a name for the new migration file.
if len(os.Args) < 2 {
log.Fatalln("no name given")
}
// Create a local migration directory.
dir, err := migrate.NewLocalDir("migrations")
if err != nil {
log.Fatalln(err)
}
// Load the graph.
graph, err := entc.LoadGraph("./ent/schema", &gen.Config{})
if err != nil {
log.Fatalln(err)
}
tbls, err := graph.Tables()
if err != nil {
log.Fatalln(err)
}
// Open connection to the database.
drv, err := sql.Open("mysql", "root:pass@tcp(localhost:3306)/ent")
if err != nil {
log.Fatalln(err)
}
// Inspect the current database state and compare it with the graph.
m, err := schema.NewMigrate(drv, schema.WithDir(dir))
if err != nil {
log.Fatalln(err)
}
if err := m.NamedDiff(context.Background(), os.Args[1], tbls...); err != nil {
log.Fatalln(err)
}
}
现在我们只需要创建迁移目录并执行上述 Go 文件:
mkdir migrations
go run -mod=mod main.go initial
你会在 migrations 目录下看到两个新文件:<timestamp>_initial.down.sql 和 <timestamp>_initial.up.sql。x.up.sql 用于创建数据库版本 x,而 x.down.sql 用于回滚到之前的版本。
CREATE TABLE `users` (`id` bigint NOT NULL AUTO_INCREMENT, `username` varchar(191) NOT NULL, PRIMARY KEY (`id`), UNIQUE INDEX `user_username` (`username`)) CHARSET utf8mb4 COLLATE utf8mb4_bin;
DROP TABLE `users`;
迁移应用
要在数据库上应用这些迁移,先像文档里那样安装 golang-migrate/migrate 工具,然后运行以下命令检查一切是否正常。
migrate -help
Usage: migrate OPTIONS COMMAND [arg...]
migrate [ -version | -help ]
Options:
-source Location of the migrations (driver://url)
-path Shorthand for -source=file://path
-database Run migrations against this database (driver://url)
-prefetch N Number of migrations to load in advance before executing (default 10)
-lock-timeout N Allow N seconds to acquire database lock (default 15)
-verbose Print verbose logging
-version Print version
-help Print usage
Commands:
create [-ext E] [-dir D] [-seq] [-digits N] [-format] NAME
Create a set of timestamped up/down migrations titled NAME, in directory D with extension E.
Use -seq option to generate sequential up/down migrations with N digits.
Use -format option to specify a Go time format string.
goto V Migrate to version V
up [N] Apply all or N up migrations
down [N] Apply all or N down migrations
drop Drop everything inside database
force V Set version V but don't run migration (ignores dirty state)
version Print current migration version
现在我们可以执行初始迁移并将数据库与模式同步:
migrate -source 'file://migrations' -database 'mysql://root:pass@tcp(localhost:3306)/ent' up
<timestamp>/u initial (349.256951ms)
工作流
为了演示使用版本化迁移的常规工作流,我们将同时编辑模式图并为其生成迁移变更,还手动创建一组迁移文件以向数据库注入一些数据。首先,我们会添加一个 Group 模式并为现有 User 模式建立多对多关系,然后创建一个包含 admin User 的 admin Group。请按如下所示修改:
package schema
import (
"entgo.io/ent"
"entgo.io/ent/schema/edge"
"entgo.io/ent/schema/field"
"entgo.io/ent/schema/index"
)
// User holds the schema definition for the User entity.
type User struct {
ent.Schema
}
// Fields of the User.
func (User) Fields() []ent.Field {
return []ent.Field{
field.String("username"),
}
}
// Edges of the User.
func (User) Edges() []ent.Edge {
return []ent.Edge{
edge.From("groups", Group.Type).
Ref("users"),
}
}
// Indexes of the User.
func (User) Indexes() []ent.Index {
return []ent.Index{
index.Fields("username").Unique(),
}
}
package schema
import (
"entgo.io/ent"
"entgo.io/ent/schema/edge"
"entgo.io/ent/schema/field"
"entgo.io/ent/schema/index"
)
// Group holds the schema definition for the Group entity.
type Group struct {
ent.Schema
}
// Fields of the Group.
func (Group) Fields() []ent.Field {
return []ent.Field{
field.String("name"),
}
}
// Edges of the Group.
func (Group) Edges() []ent.Edge {
return []ent.Edge{
edge.To("users", User.Type),
}
}
// Indexes of the Group.
func (Group) Indexes() []ent.Index {
return []ent.Index{
index.Fields("name").Unique(),
}
}
更新完模式后,运行下面的命令生成新的迁移文件:
go run -mod=mod main.go add_group_schema
再一次,你将在 migrations 目录下看到两个新文件:<timestamp>_add_group_schema.down.sql 和 <timestamp>_add_group_schema.up.sql。
CREATE TABLE `groups` (`id` bigint NOT NULL AUTO_INCREMENT, `name` varchar(191) NOT NULL, PRIMARY KEY (`id`), UNIQUE INDEX `group_name` (`name`)) CHARSET utf8mb4 COLLATE utf8mb4_bin;
CREATE TABLE `group_users` (`group_id` bigint NOT NULL, `user_id` bigint NOT NULL, PRIMARY KEY (`group_id`, `user_id`), CONSTRAINT `group_users_group_id` FOREIGN KEY (`group_id`) REFERENCES `groups` (`id`) ON DELETE CASCADE, CONSTRAINT `group_users_user_id` FOREIGN KEY (`user_id`) REFERENCES `users` (`id`) ON DELETE CASCADE) CHARSET utf8mb4 COLLATE utf8mb4_bin;
DROP TABLE `group_users`;
DROP TABLE `groups`;
现在,你可以编辑生成的文件来添加种子数据,也可以为此创建新的文件。我选择后者:
migrate create -format unix -ext sql -dir migrations seed_admin
[...]/ent-versioned-migrations/migrations/<timestamp>_seed_admin.up.sql
[...]/ent-versioned-migrations/migrations/<timestamp>_seed_admin.down.sql
现在你可以编辑这些文件,添加创建 admin Group 和 User 的语句:
INSERT INTO `groups` (`id`, `name`) VALUES (1, 'Admins');
INSERT INTO `users` (`id`, `username`) VALUES (1, 'admin');
INSERT INTO `group_users` (`group_id`, `user_id`) VALUES (1, 1);
DELETE FROM `group_users` where `group_id` = 1 and `user_id` = 1;
DELETE FROM `groups` where id = 1;
DELETE FROM `users` where id = 1;
再次应用迁移,然后完成:
migrate -source file://migrations -database 'mysql://root:pass@tcp(localhost:3306)/ent' up
<timestamp>/u add_group_schema (417.434415ms)
<timestamp>/u seed_admin (674.189872ms)
结束语
在本帖中,我们演示了使用 golang-migrate/migrate 的 Ent 版本化迁移的一般工作流。我们创建了一个小的示例模式,生成了相应的迁移文件,并学习了如何应用它们。现在你已了解工作流以及如何添加自定义迁移文件。
如有疑问?需要入门帮助?欢迎加入我们的 Discord 服务器 或 Slack 频道。
- 订阅我们的 Newsletter
- 关注我们的 Twitter
- 在 #ent 上加入我们的 Gophers Slack
- 加入我们的 Ent Discord 服务器


























