跳到主要内容
版本:2.0.x

Entity 优先工作流程

2.0.0

什么是 Entity 优先?

SeaORM 过去采用 schema 优先方法:即你先设计数据库表并编写迁移脚本,然后从该 schema 生成 entity。

Entity 优先翻转了这一流程:你手写 entity 文件,让 SeaORM 为你生成表和外键。

你只需在创建数据库连接后,将以下内容添加到 main.rs 中:

let db = &Database::connect(db_url).await?;
// 将数据库架构与实体定义同步
db.get_schema_registry("my_crate::entity::*").sync(db).await?;

这需要两个 feature 标志 schema-syncentity-registry,我们将解释它们的作用。

实体注册

提示

"my_crate::entity::*" 必须与 Cargo.toml 中你的 crate 名称匹配:

Cargo.toml
[package]
name = "my_crate"

或者,你可以执行以下操作来获取当前 crate:

// 返回调用者的 crate
db.get_schema_registry(module_path!().split("::").next().unwrap())

上述函数 get_schema_registry 展开为以下内容:

db.get_schema_builder()
.register(comment::Entity)
.register(post::Entity)
.register(profile::Entity)
.register(user::Entity)
.sync(db)
.await?;

你可能会想:在编译时,SeaORM crate 本身对 entity 一无所知,SeaORM 如何识别我的 entity?

请放心,这里没有源文件扫描或其他 hack——这是由出色的 inventory crate 实现的。inventory crate 通过将项目(称为插件)注册到链接器收集的 section 中工作。

在编译时,每个 Entity 模块都会将其自身连同其模块路径和一些元数据注册到全局 inventory。在运行时,SeaORM 然后过滤你请求的 Entity 并构建 SchemaBuilder

EntityRegistry 完全是可选的,只是为了提供额外的便利,你也可以像上面那样手动 register Entity。

解析 Entity 关系

如果你还记得上一篇文章,你会注意到 comment 有一个引用 post 的外键。由于 SQLite 不允许事后添加外键,post 表必须在 comment 表之前创建。

这就是 SeaORM 的亮点:它自动从你的 entity 构建依赖图,并确定创建表的正确拓扑顺序,因此你不必在脑海中跟踪它们。

架构同步实战

第二个 feature schema-sync 将内存中的 entity 定义与实时数据库架构进行比较,检测缺失的表、列和键,并以幂等方式创建它们——无论你运行多少次 sync,架构都会收敛到相同状态。

让我们看看不同的场景:

添加表

假设你在 mod.rs 下添加了一个新的 Entity

entity/mod.rs
//! `SeaORM` Entity, @generated by sea-orm-codegen 2.0.0-rc.14

pub mod prelude;

pub mod post;
pub mod upvote; // ⬅ new entity module
..

下次你 cargo run 时,你会看到以下内容:

CREATE TABLE "upvote" ( "id" integer NOT NULL PRIMARY KEY AUTOINCREMENT, .. )

这将创建表以及任何外键。

添加列

entity/profile.rs
use sea_orm::entity::prelude::*;

#[sea_orm::model]
#[derive(Clone, Debug, PartialEq, Eq, DeriveEntityModel)]
#[sea_orm(table_name = "profile")]
pub struct Model {
#[sea_orm(primary_key)]
pub id: i32,
pub picture: String,
pub date_of_birth: Option<DateTimeUtc>, // ⬅ new column
..
}

impl ActiveModelBehavior for ActiveModel {}

下次你 cargo run 时,你会看到以下内容:

ALTER TABLE "profile" ADD COLUMN "date_of_birth" timestamp with time zone

如果要添加非空列怎么办?你可以设置 default_valuedefault_expr

#[sea_orm(default_value = 0)]
pub post_count: i32,

// 在 SQLite 中不可用
#[sea_orm(default_expr = "Expr::current_timestamp()")]
pub updated_at: DateTimeUtc,

重命名列

如果你只想在代码中重命名字段名,可以简单地重新映射列名:

pub struct Model {
..
#[sea_orm(column_name = "date_of_birth")]
pub dob: Option<DateTimeUtc>, // ⬅ renamed for brevity
}

这不会涉及任何 schema 变更。

如果你想实际重命名列,则必须添加一个特殊属性。注意你不能简单地更改字段名,因为这会被识别为添加新列。

pub struct Model {
..
#[sea_orm(renamed_from = "date_of_birth")] // ⬅ special annotation
pub dob: Option<DateTimeUtc>,
}

下次你 cargo run 时,你会看到以下内容:

ALTER TABLE "profile" RENAME COLUMN "date_of_birth" TO "dob"

不错吧?

添加外键

让我们创建一个带外键的新表:

entity/upvote.rs
use sea_orm::entity::prelude::*;

#[sea_orm::model]
#[derive(Clone, Debug, PartialEq, Eq, DeriveEntityModel)]
#[sea_orm(table_name = "upvote")]
pub struct Model {
#[sea_orm(primary_key, auto_increment = false)]
pub post_id: i32,
#[sea_orm(belongs_to, from = "post_id", to = "id")]
pub post: HasOne<super::post::Entity>,
..
}

impl ActiveModelBehavior for ActiveModel {}

下次你 cargo run 时,你会看到以下内容:

CREATE TABLE "upvote" (
"post_id" integer NOT NULL PRIMARY KEY,
..
FOREIGN KEY ("post_id") REFERENCES "post" ("id")
)

但是,如果在表已创建之后添加 post 关系,则无法为 SQLite 创建外键。关系查询仍然有效,但完全在客户端执行。

添加唯一键

现在,假设我们忘记在用户名上添加唯一约束:

entity/user.rs
use sea_orm::entity::prelude::*;

#[sea_orm::model]
#[derive(Clone, Debug, PartialEq, Eq, DeriveEntityModel)]
#[sea_orm(table_name = "user")]
pub struct Model {
#[sea_orm(primary_key)]
pub id: i32,
#[sea_orm(unique)] // ⬅ add unique key
pub name: String,
#[sea_orm(unique)]
pub email: String,
..
}

下次你 cargo run 时,你会看到以下内容:

CREATE UNIQUE INDEX "idx-user-name" ON "user" ("name")

如上一篇博客所述,你还会在 Entity 上获得一个生成的简写方法:

user::Entity::find_by_name("Bob")..

移除唯一键

好吧,你改变主意了,想移除用户名上的唯一约束:

pub struct Model {
#[sea_orm(primary_key)]
pub id: i32,
// no annotation
pub name: String,
#[sea_orm(unique)]
pub email: String,
..
}

下次你 cargo run 时,你会看到以下内容:

DROP INDEX "idx-user-name"

脚注

请注意,通常架构同步不会尝试执行任何破坏性操作,即不会对表、列和外键执行 DROP。删除索引是此处的例外。

每次应用程序启动时,都会执行完整的 schema 发现。这在生产环境中可能不可取,因此 sync 由 feature 标志 schema-sync 控制,可以根据构建配置关闭。

在迁移中使用 SchemaBuilder

你也可以在迁移中使用 SchemaBuilder

#[async_trait::async_trait]
impl MigrationTrait for Migration {
async fn up(&self, manager: &SchemaManager) -> Result<(), DbErr> {
let db = manager.get_connection();

db.get_schema_builder()
.register(note::Entity)
.apply(db)
.await
}
}

applysync 的区别在于,sync 总是检查表/列是否已存在,而 apply 则不会。因此 apply 用于初始化步骤。

由于迁移系统已经防止两次应用迁移步骤,在迁移中使用 apply 是可以的。

为确保迁移始终可以按顺序一个一个地应用,你可以为初始迁移创建一个"时间胶囊",在子模块中保留 entity 初始版本的副本。