Rust 没有一个所有项目都会使用的数据库迁移工具。原因很简单:Rust 的数据库访问层本身就有明显分流,使用 SQLx、Diesel 和 SeaORM 时,最自然的迁移方案通常也是各自生态提供的方案。
数据库迁移真正要解决的不是“如何执行一段 SQL”,而是:
- 数据库如何从版本 A 稳定变成版本 B。
- 多人协作时,迁移历史如何合并、审查和追踪。
- 应用滚动发布时,旧版本和新版本如何短暂共存。
- 失败后如何判断数据库实际状态,而不是盲目重试。
先给结论
| 场景 | 推荐 | 原因 |
|---|---|---|
| 使用 SQLx,偏 SQL-first、异步服务 | SQLx Migrations | 与 SQLx 连接池、异步运行时和 sqlx::migrate! 集成最直接 |
| 使用 Diesel | Diesel migrations | up.sql / down.sql、CLI、嵌入和 schema.rs 体系完整 |
| 使用 SeaORM | SeaORM Migration | 使用 SeaQuery 或原始 SQL 写迁移,支持异步、种子数据和独立 migration crate |
| 不想绑定 ORM,想嵌入 SQL / Rust migration | Refinery | 支持 SQL 与 Rust 模块、CLI 和嵌入,迁移历史独立于数据访问层 |
| 多语言团队、希望做 Schema-as-Code 和风险检查 | Atlas | Rust 不是它的核心绑定,但可以作为数据库平台工具统一生成、检查和执行迁移 |
| 本地原型或可随时重建的测试库 | 简单 Schema 初始化 | 不需要为一次性数据库设计完整的生产迁移流程 |
默认选择可以压缩成一句话:
- SQLx 项目就用 SQLx Migrations。
- Diesel / SeaORM 项目优先用官方迁移 crate。
- 没有 ORM 或希望迁移工具独立于业务代码时选 Refinery。
- 需要跨语言 Schema 治理和 CI 风险分析时,再考虑 Atlas。
版本化迁移是生产默认值
Rust 生态主流方案基本都是版本化迁移:每次数据库变化对应一个新的迁移文件或模块,工具在数据库中记录已经执行过的版本。
migrations/
├── 20260824190000_create_users.sql
├── 20260825100000_add_user_email.sql
└── 20260826120000_create_user_index.sql
已经在生产环境执行过的迁移应该视为不可修改的历史记录。发现问题时新增修复迁移,而不是编辑旧文件。否则本地代码和生产数据库可能都显示“版本相同”,实际执行内容却已经不同。
down、revert 或 rollback 只能表示工具具备执行反向 DDL 的能力,并不代表数据可以恢复。删除列之后再把列加回来,原来的数据不会自动出现;生产故障通常应采用向前修复,而不是直接回滚到旧 Schema。
方案一:SQLx Migrations
项目地址:github.com/launchbadge/sqlx,CLI 文档:sqlx-cli
SQLx 的迁移是典型的 SQL-first 方案。CLI 默认创建类似下面的文件:
migrations/
├── 20260824190000_create_users.sql
└── 20260825100000_add_user_email.sql
常用命令:
sqlx migrate add create_users
sqlx migrate run
sqlx migrate info
如果需要可逆迁移,可以在创建时加 -r:
sqlx migrate add -r create_users
这样会得到 .up.sql 和 .down.sql 两个文件。SQLx 也可以把迁移嵌入二进制:
static MIGRATOR: sqlx::migrate::Migrator = sqlx::migrate!();
MIGRATOR.run(&pool).await?;
默认目录是项目根目录下的 migrations,也可以显式指定路径:
static MIGRATOR: sqlx::migrate::Migrator =
sqlx::migrate!("db/migrations");
优点
- 和 SQLx 的异步连接池、PostgreSQL、MySQL、SQLite 等数据库支持自然衔接。
- 迁移目录既能被 sqlx-cli 执行,也能通过 sqlx::migrate! 嵌入应用。
- 迁移历史保存在 _sqlx_migrations 表中,并包含 checksum;已经执行的迁移被改动时,工具会发现不一致。
- 文件格式简单,适合 SQLx 的 query!、query_as! 和手写 SQL 项目。
- 通过 sqlx migrate build-script 或手写 build.rs 可以让迁移文件变化触发重新编译。
注意点
- sqlx::migrate! 是编译期嵌入,新增迁移文件后如果没有触发重新编译,运行中的二进制不会自动看到新文件。
- checksum 和迁移表配置属于生产数据的一部分。不要为了“修复一个报错”随意更换迁移目录或迁移表名。
- SQLx 的迁移不会替你判断字段重命名、回填、长时间锁和旧版本应用兼容性。
- revert 更适合本地开发和测试。生产环境删除或改写数据后,反向 SQL 通常不能恢复原始数据。
适合谁
使用 Axum、Actix Web 或其他异步 Rust Web 框架,并且愿意直接写 SQL 的团队。对于 SQLx 项目,额外引入 Diesel 或 Refinery 通常没有必要。
方案二:Diesel migrations
项目地址:diesel.rs,迁移 API:diesel_migrations
Diesel 使用目录加 up.sql / down.sql 的模式:
migrations/
└── 20260824190000_create_users/
├── up.sql
└── down.sql
常用命令:
diesel setup
diesel migration generate create_users
diesel migration run
diesel migration redo
Diesel 的迁移文件本质上仍然是原始 SQL。执行每个迁移时,默认会使用独立事务;不能放进事务的数据库操作,可以通过 metadata.toml 关闭该迁移的事务包装。
Diesel 还可以根据 schema.rs 和数据库生成一个迁移起点:
diesel migration generate --diff-schema create_users
但生成结果不是最终答案。官方文档也明确提醒,生成的迁移需要人工调整,例如补充默认值或修正业务上的改名语义。
优点
- 迁移目录结构清晰,CLI 的学习成本低。
- 迁移执行、schema.rs 生成和 Diesel 类型系统是一套完整工作流。
- 可以通过 embed_migrations! 把迁移嵌入最终二进制。
- 默认每个迁移独立事务,失败时通常可以回滚当前迁移。
- 既支持原始 SQL,也提供基于 Schema diff 生成迁移的辅助能力。
注意点
- SQL 仍然和数据库方言绑定。PostgreSQL、SQLite 和 MySQL 的建表、类型、索引语法不能因为使用了 Diesel 就自动统一。
- —diff-schema 生成的是起点,不是经过业务语义确认的生产脚本;字段重命名、数据回填和索引并发构建都需要人工处理。
- Diesel 的核心体验偏同步连接和类型安全查询。若项目本身是纯异步 SQLx,迁移工具不必为了“自动生成”而切换数据访问层。
- 如果选择应用启动时执行嵌入迁移,仍然要处理多实例并发;嵌入文件不等于并发锁。
适合谁
已经使用 Diesel 查询、schema.rs 和 Diesel CLI 的项目。Diesel 项目没有必要为了迁移单独引入通用工具,除非团队明确需要统一跨语言的数据库平台。
方案三:SeaORM Migration
项目地址:SeaORM,迁移文档:SeaORM Migration
SeaORM 把迁移放在独立的 migration crate 中,迁移可以使用 SeaQuery 的 Rust API,也可以执行原始 SQL。一个迁移包含 up 和 down 两个异步方法:
#[derive(DeriveMigrationName)]
pub struct Migration;
#[async_trait::async_trait]
impl MigrationTrait for Migration {
async fn up(&self, manager: &SchemaManager) -> Result<(), DbErr> {
manager
.create_table(
Table::create()
.table(User::Table)
.if_not_exists()
.col(ColumnDef::new(User::Id).integer().not_null().auto_increment().primary_key())
.col(ColumnDef::new(User::Name).string().not_null())
.to_owned(),
)
.await
}
async fn down(&self, manager: &SchemaManager) -> Result<(), DbErr> {
manager
.drop_table(Table::drop().table(User::Table).to_owned())
.await
}
}
SeaORM CLI 的常用命令:
sea-orm-cli migrate init
sea-orm-cli migrate generate create_users
sea-orm-cli migrate up
sea-orm-cli migrate status
sea-orm-cli migrate down
SeaORM 会在数据库中创建迁移记录表,默认名称是 seaql_migrations。官方文档推荐把应用、实体和迁移拆成 Cargo workspace 中的不同 crate;这样迁移可以作为独立 Job 或 CLI 发布,也可以由应用调用 Migrator::up。
优点
- 和 SeaORM 的异步 API、SeaQuery、实体生成及种子数据能力结合紧密。
- 迁移逻辑可以使用 Rust 条件、循环和 SeaORM API,适合需要数据迁移的场景。
- 使用独立 migration crate,迁移工具和应用启动逻辑可以分开。
- 通过 sea-orm-cli 可以初始化、生成、执行、查看状态和回滚迁移。
- SeaORM 2 还提供 Entity First Workflow,但仍应把生产变更放进可审查的版本化发布流程。
注意点
- Rust API 写 DDL 比直接 SQL 更冗长;复杂数据库特性最终可能仍然需要原始 SQL。
- 新迁移文件不仅要创建,还要按时间顺序加入 MigratorTrait::migrations 列表。漏掉这一步,文件存在也不会执行。
- 把所有数据修复逻辑写进 migration crate 会让迁移越来越像业务服务。简单结构变化优先使用 SeaQuery 或 SQL,复杂回填要明确批量、幂等和超时策略。
- 应用启动执行迁移虽然方便,但不应自动推导为多实例安全。生产更适合单独运行 migration crate。
适合谁
使用 SeaORM、偏异步、希望迁移也用 Rust API 表达的团队。SeaORM 的迁移 crate 已经覆盖了大多数日常需求,不需要再叠加 SQLx Migrations。
方案四:Refinery
项目地址:github.com/rust-db/refinery
Refinery 更接近独立的 Flyway 风格迁移器,不要求项目使用某个 ORM。它支持 SQL 文件,也支持返回 SQL 字符串的 Rust migration module:
migrations/
├── V1__create_users.sql
├── V2__add_user_email.sql
└── V3__create_user_index.rs
可以通过 embed_migrations! 把迁移编译进二进制:
mod embedded {
use refinery::embed_migrations;
embed_migrations!("./migrations");
}
embedded::migrations::runner().run(&mut connection)?;
也可以安装 CLI 执行迁移:
cargo install refinery_cli
refinery migrate -e DATABASE_URL -p ./migrations
Refinery 默认按迁移执行历史检查缺失和 divergent migration,并默认每个迁移使用独立事务;也可以把全部迁移放到一个事务中,但 MySQL 的 DDL 事务语义会限制这种做法。
优点
- 不绑定 Diesel、SQLx 或 SeaORM,适合项目已经有自定义数据库访问层的情况。
- SQL 和 Rust migration 都可以嵌入最终二进制。
- 支持同步和异步数据库驱动,迁移目录可以作为独立工具使用。
- 会检查已经应用的迁移是否出现版本、名称或 checksum 不一致。
- 采用 V1__name.sql 这类显式版本格式,迁移历史容易被其他语言团队理解。
注意点
- Refinery 没有传统意义上的 down 文件;如果要撤销已经发布的变更,需要新建一个迁移来执行反向操作。
- 迁移工具独立于 ORM,因此不会帮你同步 Diesel schema.rs、SeaORM entity 或其他代码模型。
- Rust migration 返回 SQL 字符串,不能直接把它当作普通应用业务逻辑容器;复杂数据迁移需要关注事务、连接生命周期和可重试性。
- 迁移目录被嵌入后二进制后,修改本地文件不会影响已经构建好的镜像,发布时必须重新构建。
适合谁
使用 tokio-postgres、rusqlite、mysql_async、Tiberius 或自定义数据访问层,又希望迁移工具独立于 ORM 的团队。尤其适合想采用“只向前增加迁移,不在生产执行 down”的团队。
Atlas:不是 Rust 专属,但适合数据库平台化
项目地址:Atlas
Atlas 是语言无关的 Schema-as-Code 和迁移工具。它可以从 SQL、HCL 或 ORM Schema 计算差异,生成版本化迁移,并在 CI 中检查删除表、删除列、非空约束、表锁和数据依赖等问题。
它不是 Rust 项目的默认迁移库,因为 SQLx、Diesel、SeaORM 和 Refinery 都能独立完成版本化迁移。但以下场景值得考虑:
- 公司同时维护 Rust、Go、Java 等多个后端,需要统一迁移审核规则。
- 数据库团队希望在 Pull Request 阶段检查所有服务的 Schema 变更。
- 团队愿意把迁移生成、lint 和 apply 拆成数据库平台流水线。
Atlas 的自动 diff 也不能识别所有业务语义。把 name 改成 display_name 时,工具可能生成删除旧列、增加新列;正确方案可能是增加新列、双写、回填,最后再删除旧列。生成的 SQL 仍然需要人工审查。
关键能力对比
| 产品 | SQL-first | Rust migration | 迁移嵌入 | 回滚模型 | ORM 绑定 | CI / Schema 检查 | 适合生产默认值 |
|---|---|---|---|---|---|---|---|
| SQLx Migrations | 强 | 弱,主要执行 SQL | sqlx::migrate! | 可选 .down.sql | SQLx | 需自行组合 | 高 |
| Diesel migrations | 强 | 支持 Rust migration source | embed_migrations! | down.sql | Diesel | schema.rs + CLI | 高 |
| SeaORM Migration | 支持 | 强,Rust + SeaQuery | migration crate / 应用集成 | up / down | SeaORM | CLI + 类型模型 | 高 |
| Refinery | 强 | 支持 Rust 模块 | embed_migrations! | 新增 forward migration | 无 | divergence / checksum | 高 |
| Atlas | 支持 | 通过 Schema / 外部集成 | 不是核心用法 | 版本化 / forward fix | 无 | 内置 diff / lint | 高 |
这里的“适合生产默认值”指工具能否作为生产迁移流程的基础,不代表可以把迁移直接塞进应用启动,更不代表生成的 DDL 不会锁表。
推荐的生产工作流
1. 迁移作为独立发布步骤
不要让每一个 Web 实例都在启动时抢着执行迁移。比较稳妥的流程是:
构建应用和迁移工具
↓
Pull Request:审查迁移 SQL / Rust migration
↓
CI:空库执行全部迁移,检查状态和失败路径
↓
部署 Job:只运行一次迁移
↓
发布兼容新旧 Schema 的应用
如果确实需要应用启动迁移,应明确使用数据库锁或平台层的单实例 Job,并验证目标数据库的锁和事务语义。把迁移嵌入二进制只解决“文件是否随程序发布”,不解决“谁来执行”和“多个实例是否并发执行”。
2. 使用 expand / migrate / contract
例如把 users.name 改成 users.display_name,不要在一个迁移里直接删列:
- 增加 display_name,先允许为空或提供安全默认值。
- 新旧版本应用同时写入必要的字段。
- 分批回填历史数据,观察错误和延迟。
- 代码只读取新列,并确认旧版本已经下线。
- 最后再用独立迁移删除旧列。
字段重命名、非空约束、唯一索引和大表索引构建,都是比“迁移工具能不能执行”更重要的发布问题。
3. 正确处理事务边界
很多迁移默认在事务中执行,但数据库并不保证所有 DDL 都能回滚。比如某些数据库版本的索引并发构建、数据库级对象和表重写操作不能放在普通事务里。
每个迁移都应该确认:
- 这条 DDL 是否允许出现在事务中。
- 失败后数据库会处于什么状态。
- 是否需要拆成多个迁移或关闭事务。
- 数据回填是否应该独立于 Schema 变更,并且支持分批重试。
4. 验证迁移历史
最小 CI 检查可以按工具组合:
# SQLx
sqlx migrate run
cargo sqlx prepare --check
# Diesel
diesel migration run
diesel migration redo
# SeaORM
sea-orm-cli migrate up
sea-orm-cli migrate status
# Refinery
refinery migrate -e DATABASE_URL -p ./migrations
测试数据库应尽量使用和生产相同的大版本。SQLite 上成功,不代表 PostgreSQL 或 MySQL 上的锁、事务、类型和 ALTER TABLE 行为相同。
最终选择
- SQLx 项目:SQLx Migrations。SQL 目录、异步连接池和二进制嵌入已经足够完整。
- Diesel 项目:Diesel migrations。使用 diesel_migrations 保持 CLI、迁移和 schema.rs 一致。
- SeaORM 项目:SeaORM Migration。把 migration crate 独立出来,生产通过 Job 或专用 CLI 执行。
- 不使用 ORM,或者希望迁移工具独立:Refinery。要是团队只接受 forward migration,它的模型更符合生产习惯。
- 多语言数据库治理:Atlas。用它统一 diff、lint 和审核,再决定实际由哪种执行器 apply。
Rust 数据库迁移的关键,不是选择最“Rust 化”的 API,而是让迁移历史可审查、可重复执行、可观测,并与应用发布兼容。能用 SQL 清晰表达的结构变化,就不要为了追求类型安全把它包装成一层很长的 Rust 代码;真正需要业务逻辑的回填,才值得使用 Rust migration。