迁移系统
迁移系统(Migration)简单来说就是数据库的版本控制工具。就像 Git 记录代码改动一样,Migration 记录数据库表结构的改动(新建表、加字段、删索引等)。
1. 完整的工作区 (Workspace) 结构
在 Rust 中,SeaORM 官方强烈建议使用 Workspace 模式。这能让你的“业务代码”和“数据库迁移代码”彻底解耦。
推荐的目录树如下:
my_project/
├── Cargo.toml # 根目录虚拟清单
├── src/ # 你的业务逻辑 (App Crate)
│ └── main.rs
└── migration/ # 迁移逻辑 (Migration Crate)
├── Cargo.toml
└── src/
├── lib.rs # 定义迁移器
├── main.rs # CLI 入口
└── m20240110_000001_create_user_table.rs # 具体的表变动记录
2. 第一步:初始化与配置
首先,安装工具并创建迁移目录。
安装 CLI
cargo install sea-orm-cli
初始化
在你的项目根目录下运行:
sea-orm-cli migrate init
配置 migration/Cargo.toml
这是最容易出错的地方。你需要为迁移脚本开启 PostgreSQL 支持:
[dependencies.sea-orm-migration]
version = "1.1.0"
features = [
"runtime-tokio-rustls", # 异步运行时
"sqlx-postgres", # 明确指定使用 Postgres
]
3. 第二步:编写迁移逻辑 (核心细节)
迁移文件(如 m..._create_table.rs)是纯 Rust 代码。SeaORM 使用 SeaQuery(一种 DSL)来描述 SQL。
up 函数:创建表
假设我们要创建一个用户表:
use sea_orm_migration::prelude::*;
//SeaORM 需要给每个迁移任务起个名字(存到 PostgreSQL 的 seaql_migrations 表里)。这个宏会自动把你的文件名(比如 m20240110_...)提取出来作为名字,省得你手动写。
#[derive(DeriveMigrationName)]
pub struct Migration;
// 实现 MigrationTrait 来定义具体的迁移逻辑
#[async_trait]
impl MigrationTrait for Migration {
async fn up(&self, manager: &SchemaManager) -> Result<(), DbErr> {
// 使用 manager 创建表
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::Username).string().not_null().unique_key())
.col(ColumnDef::new(User::CreatedAt).date_time().default(Expr::current_timestamp()))
.to_owned(),
)
.await
}
async fn down(&self, manager: &SchemaManager) -> Result<(), DbErr> {
// 回滚操作:删除表
manager
.drop_table(Table::drop().table(User::Table).to_owned())
.await
}
}
// 定义表和列的枚举,方便在 Rust 里引用. 在 SQL 里我们需要写 user(表名)、id(列名),如果不加这个,你得手写字符串 "user", "id"。加了它,你就可以写 User::Table 或 User::Id。如果有拼写错误,编译器会直接报错,这非常安全。
#[derive(DeriveIden)]
enum User {
Table,
Id,
Username,
CreatedAt,
}
在数据库迁移(Migration)中,up 和 down 就像是版本控制里的 “前进” 和 “后退”。你可以把它们想象成一对“反义词”操作:up 负责建立,down 负责拆除。
1. fn up
当你运行 sea-orm-cli migrate up 时,执行的就是这个函数。
- 目的:改变数据库的结构,让它变成你想要的新样子。
- 常见操作:
- 创建新表(
create_table)。 - 给现有的表增加新列(
alter_table)。 - 创建索引或外键。
- 创建新表(
2. fn down
当你发现这次改动有问题,或者想回到上一个数据库版本,运行 sea-orm-cli migrate down,执行的就是这个函数。
- 目的:把
up函数做过的事情原样撤销,让数据库回到没改之前的状态。 - 常见操作:
- 如果
up是创建表,down就是删除表(drop_table)。 - 如果
up是加了一列,down就是删掉那一列。
- 如果
- 重要性:对称性。为了保证数据库的整洁,
down逻辑必须写得准确。如果up建了表但down没写删表,数据库就会留下“垃圾”。
3. 它们是如何工作的?
SeaORM 会在你的 PostgreSQL 数据库里自动维护一张表,叫 seaql_migrations。
| 版本号 (Version) | 应用时间 (Applied At) |
|---|---|
| m20220101_000001 | 2024-05-20 … |
- **当你执行
up**:SeaORM 运行up函数。如果成功,就把这个文件名(版本号)记入这张表。下次再跑up时,发现表里已经有了,它就会跳过,不会重复执行。 - **当你执行
down**:SeaORM 查找表里最后一条记录,找到对应的文件,执行它的down函数。如果成功,就从表里删掉这条记录。
4. 深入代码细节
让我们看看你刚才提供的代码块里,这两个函数具体在干什么:
// --- UP: 增加功能 ---
async fn up(&self, manager: &SchemaManager) -> Result<(), DbErr> {
manager
.create_table( // 动作:创建表
Table::create() // 建造者模式开始
.table(User::Table) // 表名:user
.col(...) // 具体的列定义
.to_owned()
)
.await
}
// --- DOWN: 撤销功能 ---
async fn down(&self, manager: &SchemaManager) -> Result<(), DbErr> {
manager
.drop_table( // 动作:删除表(撤销 up 的创建)
Table::drop()
.table(User::Table) // 指定要删哪张表
.to_owned()
)
.await
}
4. 第三步:如何执行迁移
你有两个选择:命令行或代码内运行。
A. 命令行方式 (手动)
你需要设置环境变量来告诉 CLI 你的 PostgreSQL 连接串:
# 设置环境变量 (Linux/macOS)
export DATABASE_URL="postgres://user:pass@localhost:5432/my_db"
# 查看当前迁移状态
sea-orm-cli migrate status
# 执行所有待处理的迁移
sea-orm-cli migrate up
# 撤销最后一次迁移
sea-orm-cli migrate down
B. 代码方式 (自动化)
在 src/main.rs 中,连接数据库后立即执行迁移。这样每次你部署新代码,数据库都会自动更新。
use migration::{Migrator, MigratorTrait}; // 引入你的 migration crate
#[tokio::main]
async fn main() {
let db = sea_orm::Database::connect("postgres://...").await.unwrap();
// 这一行会自动运行所有未执行的 .rs 迁移文件
Migrator::up(&db, None).await.expect("迁移失败");
println!("数据库已就绪");
}
5. 迁移系统的生命周期 (关键点)
为了防止弄乱 PostgreSQL,请务必理解这三个概念:
seaql_migrations表:SeaORM 会自动在你的数据库里建这张表。它记录了哪些.rs文件已经跑过了。不要手动删它。- 不可变性:一旦某个迁移文件已经
up到了生产环境,不要修改它。如果你想加字段,应该建一个新的迁移文件。 - Iden 宏:代码最后的
enum User { Table, Id... }是为了避免在代码里写硬编码字符串(如"username"),增加类型安全性。