编程 把数据库 schema 当代码管:Atlas 的声明式与版本化迁移怎么选

2026-10-11 00:03:47

把数据库 schema 当代码管:Atlas 的声明式与版本化迁移

手写的 ALTER TABLE 散在发布脚本里,预发和生产悄悄漂移,删列丢数据没人拦——这是多数团队管 schema 的现状。Atlas 把这套流程改成代码化:定义期望状态,引擎计算迁移计划,破坏性变更在 apply 之前被拦下。

  • GitHub:
  • 官网:

Atlas 的定位是 "Declarative database schema management — define your desired state, Atlas computes the plan",类比的话就是「数据库界的 Terraform」。

声明式:删一列会被直接拒绝

编辑 schema/categories.sql,删掉 category_description 列,运行:

atlas schema apply --env local

输出:

Planning migration statements (1 in total):
-- modify "categories" table:
-> ALTER TABLE "categories" DROP COLUMN "category_description";
Analyzing planned statements (1 in total):
-- destructive change detected: data loss possible
DS102: column "category_description" contains data
Destructive change blocked.
Data loss detected. Migration rejected.

计划生成出来,但因为有数据丢失风险,apply 被拒绝。对应的老做法是手工写 ALTER TABLE、没有破坏性变更审查、环境之间 schema 漂移。

Schema 可以用 SQL 或 HCL 描述。比如 users.sql 里直接引用 functions.sql:

-- atlas:import functions.sql
CREATE TABLE users (
id serial NOT NULL,
name varchar(255) NOT NULL,
email varchar(255) UNIQUE NOT NULL,
phone varchar(20),
bio text,
PRIMARY KEY (id)
);
CREATE TABLE user_logs (
id serial NOT NULL,
user_id int NOT NULL,
body text NOT NULL,
PRIMARY KEY (id),
CONSTRAINT user_fk FOREIGN KEY (user_id) REFERENCES users(id)
);

Atlas 生成的迁移文件 20261009173911_add_user_logs.sql:

ALTER TABLE "users" ADD COLUMN "phone" varchar(20) NULL, ADD COLUMN "bio" text NULL;
CREATE TABLE "user_logs" (...);

Schema 定义支持 SQL、HCL、Python、Node.js、Java、Go、C#、PHP,也可以从 ORM(GORM、Drizzle、Django、SQLAlchemy)反向读取。目标数据库覆盖 PostgreSQL、MySQL、SQL Server、MariaDB、ClickHouse、Redshift、SQLite、Oracle、Snowflake、Spanner、Databricks、CockroachDB、Aurora DSQL、YugabyteDB、Azure Fabric。

SQLite 改列默认值必须重建表

users 表结构是 id INTEGER, greeting TEXT,现在想给 greeting 加默认值 "shalom"。SQLite 不支持修改已有列的默认值,常见绕法是建新表、拷旧行、drop 旧表。

声明式写法只改 HCL:

table "users" {
schema = schema.main
column "id" {
type = int
}
column "greeting" {
type    = text
default = "shalom"
}
}

Atlas 引擎生成四步计划:

-- Create "new_users" table
CREATE TABLE `new_users` (`id` int NOT NULL, `greeting` text NOT NULL DEFAULT 'shalom')
-- Copy rows from old table "users" to new temporary table "new_users"
INSERT INTO `new_users` (`id`,`greeting`) SELECT `id`, IFNULL(`greeting`,'shalom') AS `greeting` FROM `users`
-- Drop "users" table after copying rows
DROP TABLE `users`
-- Rename temporary table "new_users" to "users"
ALTER TABLE `new_users` RENAME TO `users`

两个命令的区别:

  • atlas schema plan:审阅并批准迁移计划,apply 阶段跳过审批流。
  • atlas schema apply:应用变更,默认执行前需要审批,可以是人工批准,也可以是基于 review policy 的 lint 诊断。被 plan 预先批准的变更会自动应用。

两者都支持 Atlas Actions(GitHub Actions、GitLab CI、Azure DevOps)。

版本化:每次变更签入一个迁移文件

当应用同时部署到多个远程环境、开发团队无法访问甚至无法控制目标库时,声明式那套「连目标库 + 人工实时批准计划」跑不起来。这时用版本化迁移,SQL 文件描述 how,每个文件带唯一版本号和描述,Flyway、Liquibase、golang-migrate 都是这个路子。

三个命令:

  • atlas migrate diff:对比迁移目录当前状态和期望 schema,自动生成迁移文件。
  • atlas migrate lint:分析迁移文件里的潜在问题。
  • atlas migrate apply:应用待执行的迁移文件。

版本化的代价是规划负担落到开发者身上,写安全迁移需要专业度,比如上面 SQLite 那种重建表的情况。Atlas 推荐第三种组合——Versioned Migration Authoring:仍然声明期望状态,用引擎规划安全迁移,但不把规划和执行耦合,把计划写成普通迁移文件,签入源码控制,允许手工微调,走 code review。

两条路怎么混用

本地开发走声明式:改 HCL 或 SQL 文件,对本地库 atlas schema apply 快速迭代,不用写迁移文件。

需要共享时切版本化:对更新后的期望状态跑 atlas migrate diff,生成迁移文件写进 migrations 目录,commit 进 PR,走 code review、迁移 lint 和 CI/CD 里 atlas migrate apply。

什么时候必须用版本化

  • On-premise / 打包软件部署:客户自己安装管理,apply 阶段没人能审阅批准计划。版本化保证经创建、审阅、合并到主分支的迁移文件按原样在客户库执行,基于 git 历史可重复。
  • CI/CD 数据库访问受限:runner 因网络隔离、安全策略或合规要求连不上生产/预发,无法动态生成计划。版本化基于迁移目录前一状态预生成,apply 前不需要检查目标库 schema。
  • 大团队并发改 schema:迁移目录完整性文件 atlas.sum 防止冲突,通过 merge conflict 检测保证线性历史,需要 rebase。
  • 跨环境一致:多个环境、客户、实例应用完全相同的变更。
  • 严格变更控制与审计:每次变更需要显式的版本记录,供合规和安全审计。

CI 里 lint 能拦下什么

atlas migrate lint 逐个分析迁移的破坏性变更、表锁、数据丢失风险和并发索引违规。内建 analyzers 包括:Destructive changes、Data-dependent changes、Backward incompatible、Blocking table locks、Concurrent index、Table copy detection、Constraint deletion、Naming conventions、SQL injection、Transaction safety。

PR 里检查输出的典型例子:

Destructive changes detected (Dropping non-virtual column "email")
Data dependent changes detected (Adding a non-nullable "varchar" column "phone" without a DEFAULT value)
Blocking table change detected (Changing column type from "int" to "bigint" requires table rewrite)
Concurrent index violation (Creating index non-concurrently acquires SHARE lock, blocking writes)

Security、Data、DataOps 也可以声明

Security as Code 用 HCL 定义 role、user 和 permission:

role "app_readonly" {
comment = "Read-only access"
}
role "app_writer" {
member_of = [role.app_readonly]
}
user "api_service" {
member_of = [role.app_writer, role.rds_iam]
}
permission {
for_each   = [table.users, table.orders]
for        = each.value
to         = role.app_readonly
privileges = [SELECT]
}

policy.hcl 里可以写策略规则,例如 rule "schema" "no-superuser",用 predicate.not_superuser 断言并在违反时给出消息;还有 rule "schema" "no-grantable"。

流程是 Define → Plan(atlas schema plan)→ Review(CI 里跑 policy 检查)→ Deploy(Kubernetes operator 应用)。

Data as Code 管理声明式数据:Seed 查表、比较 desired 与 live、生成 DML,三种同步模式分别是 INSERT(仅添加)、UPSERT(添加 + 更新)、SYNC(全同步)。

data {
table = table.countries
rows = [
{ id = 1, code = "US", name = "United States" },
]
}

DataOps as Code 把 backfill、GDPR 删除、报表都声明成代码,用 atlas script exec 运行,事务模式 ONE_UNIT,出错策略 on_error ROLLBACK:

script "exec" "archive_dormant" {
condition "have_dormant" { ... }
query "dormant" { rows { id = int } }
exec "purge" { expect_rows = length(query.dormant.rows) }
}

Cloud Native 与 Agents

Terraform provider 是 ariga/atlas ~> 0.9.7:

provider "atlas" {
dev_url = "docker://postgres/15/myapp"
}

data "atlas_schema" "sql" {
src = "file://${path.module}/schema.sql"
}

resource "atlas_schema" "postgres" {
url = "..."
hcl = data.atlas_schema.sql.hcl
}

集成面覆盖 Terraform、Kubernetes、Argo CD、Crossplane、GitHub Actions、GitLab CI、Azure DevOps。

Agents 侧,Atlas 为 Claude Code、Codex、Cursor、Copilot 提供了 skills:

claude plugin marketplace add ariga/atlas
claude plugin install atlas@ariga
npx skills add ariga/atlas -a codex -y

装好后用 /atlas:atlas-onboard 起步。


项目地址:GitHub ariga/atlas · atlasgo.io

复制全文 生成海报 Schema-as-Code 数据库迁移 Atlas MySQL Go 运维

推荐文章

程序员茄子在线接单