DeriveEntityModel 与 dense 格式
主线位置:定义主线本身。你写一个
Modelstruct,DeriveEntityModel从它生成Entity、Column、PrimaryKey、ActiveModel和 上一页 那些 trait 的实现;2.0 的#[sea_orm::model]在它前面再改写一次 struct,多生成一组给关联数据用的类型。查询主线的最后一步Model::from_query_result、写入主线的第一步ActiveModel的字段,都是这一页的生成代码。
flowchart TB src["Model struct"] --> attr["sea_orm::model 属性"] attr -->|dense| split["Model + ModelEx"] src -->|展开写法| dem[DeriveEntityModel] split --> dem split --> dmx["DeriveModelEx 等"] dem --> p1["Column / Entity / PrimaryKey"] dem --> p2["FromQueryResult / ModelTrait"] dem --> p3[ActiveModel] dmx --> p4["ModelEx / COLUMN / Loader"]
两个实验实体映射同一张表,只差写法(src/bin/expand_plain.rs、src/bin/expand_dense.rs)。展开写法要自己写一个 Relation 枚举,没有关系就写空的:
// 1.x-style ("expanded") entity: DeriveEntityModel + hand-written Relation.
mod post {
use sea_orm::entity::prelude::*;
#[derive(Clone, Debug, PartialEq, Eq, DeriveEntityModel)]
#[sea_orm(table_name = "post")]
pub struct Model {
#[sea_orm(primary_key)]
pub id: i32,
pub title: String,
pub body: Option<String>,
}
#[derive(Copy, Clone, Debug, EnumIter, DeriveRelation)]
pub enum Relation {}
impl ActiveModelBehavior for ActiveModel {}
}
dense 写法去掉 Relation,在 derive 前面加一行属性:
// 2.0 dense entity: #[sea_orm::model] on top of DeriveEntityModel.
mod post {
use sea_orm::entity::prelude::*;
#[sea_orm::model]
#[derive(Clone, Debug, PartialEq, Eq, DeriveEntityModel)]
#[sea_orm(table_name = "post")]
pub struct Model {
#[sea_orm(primary_key)]
pub id: i32,
pub title: String,
pub body: Option<String>,
}
impl ActiveModelBehavior for ActiveModel {}
}
用 cargo expand --offline --bin <name> 展开(M2 Max、macOS;程序用 rustc 1.99.0 编译运行,展开用 cargo-expand 1.0.104 调的 nightly 工具链 rustc 1.101.0-nightly;sea-orm = "=2.0.4",features 同上一页,没开 entity-registry),展开写法 1037 行,dense 写法 2206 行。下面只引用关键的几行,// ... 是删掉的部分。
一个 derive,三段展开
derive_entity_model 先检查名字,再把三个展开函数的输出接在一起:
pub fn derive_entity_model(input: TokenStream) -> TokenStream {
let DeriveInput {
vis,
ident,
data,
attrs,
..
} = parse_macro_input!(input as DeriveInput);
if ident != "Model" {
panic!("Struct name must be Model");
}
let mut ts: TokenStream = derives::expand_derive_entity_model(&vis, &data, &attrs)
.unwrap_or_else(Error::into_compile_error)
.into();
ts.extend::<TokenStream>(
derives::expand_derive_model(&ident, &data, &attrs)
.unwrap_or_else(Error::into_compile_error)
.into(),
);
ts.extend::<TokenStream>(
derives::expand_derive_active_model(&vis, &ident, &data)
.unwrap_or_else(Error::into_compile_error)
.into(),
);
ts
}
| 段 | 展开函数 | 生成 | 展开写法里的行 |
|---|---|---|---|
| 1 | expand_derive_entity_model | Column 枚举及 ColumnTrait;Entity 及 EntityName;PrimaryKey 及 PrimaryKeyTrait | 61–486 |
| 2 | expand_derive_model(impl_from_query_result、impl_model_trait) | FromQueryResult for Model、ModelTrait for Model | 487–645 |
| 3 | expand_derive_active_model | ActiveModel struct、ActiveModelTrait、From<Model>、TryFrom<ActiveModel> for Model | 646–903 |
剩下的行是 #[derive(Clone, Debug, ...)] 的标准展开、你自己写的 Relation(904–1015),以及 impl ActiveModelBehavior。第 1 段并不直接写出所有实现:它生成的 Column 带着 #[derive(EnumIter, DeriveColumn)],Entity 带着 #[derive(DeriveEntity)],PrimaryKey 带着 #[derive(EnumIter, DerivePrimaryKey)],这些内层 derive 再展开出 Iden、FromStr、EntityTrait、PrimaryKeyToColumn 等。所以也可以不用 DeriveEntityModel,手写 Entity、Column、PrimaryKey、Model 几个类型,分别 derive DeriveEntity、DeriveColumn、DerivePrimaryKey、DeriveModel、DeriveActiveModel,再手写 ColumnTrait 和 PrimaryKeyTrait(上游的 src/tests_cfg/cake_expanded.rs 就是这么写的,它和宏的实际输出不完全一致,以 cargo expand 为准)。
第一段:Column、Entity、PrimaryKey
每个字段变成 Column 的一个变体,变体名是字段名的 UpperCamelCase:
pub enum Column {
/// Generated by sea-orm-macros
Id,
/// Generated by sea-orm-macros
Title,
/// Generated by sea-orm-macros
Body,
}
ColumnTrait::def 是按变体的 match,列类型取自字段的 Rust 类型。Option<String> 被剥掉 Option,换成 .nullable():
impl sea_orm::prelude::ColumnTrait for Column {
type EntityName = Entity;
fn def(&self) -> sea_orm::prelude::ColumnDef {
match self {
Self::Id => {
sea_orm::prelude::ColumnTypeTrait::def(
<i32 as sea_orm::sea_query::ValueType>::column_type(),
)
}
// ...
Self::Body => {
sea_orm::prelude::ColumnTypeTrait::def(
<String as sea_orm::sea_query::ValueType>::column_type(),
)
.nullable()
}
}
}
// ...
fn save_as(
&self,
val: sea_orm::sea_query::Expr,
) -> sea_orm::sea_query::SimpleExpr {
match self {
_ => sea_orm::prelude::ColumnTrait::save_enum_as(self, val),
}
}
剥 Option 是在字符串上做的:宏把字段类型 quote! 成文本、去掉空白,看它是不是以 Option< 开头。写成 std::option::Option<String> 就不会被认出来,def() 里少了 .nullable()(src/bin/opt_path.rs 里两个字段分别写成 Option<String> 和 std::option::Option<String>,打印 a nullable: true, b nullable: false)。解码不受影响,两种写法读 NULL 都得到 None;受影响的是建表和 schema 同步。
Entity 和 PrimaryKey:
impl sea_orm::prelude::EntityName for Entity {
fn schema_name(&self) -> Option<&str> {
None
}
fn table_name(&self) -> &'static str {
"post"
}
// ...
}
// ...
impl PrimaryKeyTrait for PrimaryKey {
type ValueType = i32;
fn auto_increment() -> bool {
true
}
}
ValueType 是主键字段的类型原样抄过来(多个主键字段时是它们的元组),auto_increment 的推断规则见 上一页的动脑筋答案。
字段属性落在哪里
#[sea_orm(...)] 的每个字段属性在 expand_derive_entity_model 里被解析成一个局部变量,最后影响上面某一段生成代码:
| 属性 | 影响 |
|---|---|
primary_key | 加一个 PrimaryKey 变体,字段类型进 ValueType |
auto_increment = bool | auto_increment() 的返回值;不在主键上写会报 auto_increment can only be used on primary_key |
column_name = "..." | Column 变体的 as_str(),也就是 SQL 里的列名 |
enum_name = "..." | Column / PrimaryKey 变体的名字(Rust 侧),不改列名 |
column_type = "..." | def() 里的 ColumnType,代替从 Rust 类型推出来的那个;也影响强类型列选哪种包装 |
nullable、indexed、unique、unique_key、default_value、default_expr、comment、renamed_from、extra | 只在 def() 的 ColumnDef 上追加 .nullable()、.unique() 等,给建表和 schema 同步用 |
select_as = "type" | select_as() 返回 CAST(列 AS type),SELECT 列表里用 |
save_as = "type" | save_as() 返回 CAST(值 AS type),写入和条件里的值用 |
ignore | 不生成 Column 变体;FromQueryResult 里这个字段填 Default::default() |
nullable 只改 ColumnDef,不改解码:字段不是 Option 的话,读到 NULL 照样报错(见 列与值)。unique 在 dense 格式下还有一个作用,后面讲。
select_as / save_as 用一个实验看(src/bin/cast_as.rs):
mod item {
use sea_orm::entity::prelude::*;
#[sea_orm::model]
#[derive(Clone, Debug, PartialEq, Eq, DeriveEntityModel)]
#[sea_orm(table_name = "item")]
pub struct Model {
#[sea_orm(primary_key)]
pub id: i32,
#[sea_orm(select_as = "text", save_as = "citext")]
pub code: String,
}
impl ActiveModelBehavior for ActiveModel {}
}
fn main() {
let find = item::Entity::find().filter(item::Column::Code.eq("A1")).build(DbBackend::Postgres);
println!("{}", find.sql);
let ins = item::Entity::insert(item::ActiveModel { code: Set("A1".into()), ..Default::default() })
.build(DbBackend::Postgres);
println!("{}", ins.sql);
}
SELECT "item"."id", CAST("item"."code" AS text) AS "code" FROM "item" WHERE "item"."code" = CAST($1 AS citext)
INSERT INTO "item" ("code") VALUES (CAST($1 AS citext))
SELECT 列表里的列被转换并取回原名作别名,FromQueryResult 仍按 code 取值;条件右侧和 VALUES 里的参数被转换,WHERE 左侧的列本身不动。enum 列默认就走这条路(select_enum_as / save_enum_as),见 ActiveEnum 与 ValueType。
列名没写 column_name 时由字段名推出:宏先把字段名转成 UpperCamelCase 当变体名,再看「字段名」和「变体名转回 snake_case」是否相同,不同就把字段名的 snake_case 当列名。
M2 Max、macOS、rustc 1.99.0(src/bin/col_names.rs):
#![allow(non_snake_case)]
use sea_orm::{IdenStatic, Iterable};
mod odd {
use sea_orm::entity::prelude::*;
#[sea_orm::model]
#[derive(Clone, Debug, PartialEq, Eq, DeriveEntityModel)]
#[sea_orm(table_name = "odd")]
pub struct Model {
#[sea_orm(primary_key)]
pub id: i32,
pub r#type: String,
pub createdAt: i64,
pub name_: String,
#[sea_orm(column_name = "Display Name")]
pub display_name: String,
}
impl ActiveModelBehavior for ActiveModel {}
}
fn main() {
for c in odd::Column::iter() {
println!("{:<12} -> {:?}", format!("{c:?}"), c.as_str());
}
}
五行输出里,createdAt 和 name_ 两行的列名分别是什么?
第二段:FromQueryResult 与 ModelTrait
生成的 from_query_result 只是转调 from_query_result_nullable,后者对每一列先读成 Option<字段类型>:
impl sea_orm::FromQueryResult for Model {
fn from_query_result(
row: &sea_orm::QueryResult,
pre: &str,
) -> std::result::Result<Self, sea_orm::DbErr> {
Self::from_query_result_nullable(row, pre).map_err(Into::into)
}
fn from_query_result_nullable(
row: &sea_orm::QueryResult,
pre: &str,
) -> std::result::Result<Self, sea_orm::TryGetError> {
let id = row
.try_get_nullable::<
Option<i32>,
>(
pre,
sea_orm::IdenStatic::as_str(
&<<Self as sea_orm::ModelTrait>::Entity as sea_orm::entity::EntityTrait>::Column::Id,
)
.into(),
)?;
// ...
let body = row
.try_get_nullable::<
Option<Option<String>>,
>(
// ...
)?;
if true && id.is_none() && title.is_none()
&& #[allow(non_exhaustive_omitted_patterns)]
match body {
None | Some(None) => true,
_ => false,
}
{
return Err(
sea_orm::TryGetError::Null(
"All fields of nested model are null".into(),
),
);
}
Ok(Self {
id: id
.ok_or_else(|| sea_orm::DbErr::Type(
::alloc::__export::must_use({
::alloc::fmt::format(
format_args!(
"Missing value for column \'{0}\'",
// ...
),
)
}),
))?,
// ...
})
}
}
三步:每列读成 Option(body 本身可空,于是是 Option<Option<String>>);所有列都是空就返回 TryGetError::Null,让外层的 Option<Model> 变成 None;否则逐个 ok_or_else,缺值报 Missing value for column '...'。列名来自 Column::X.as_str(),pre 是 join 时的前缀。这样绕一圈是为了让 LEFT JOIN 另一侧整行是 NULL 时得到 None 而不是报错。它对一列 NULL 落进非 Option 字段、对 SELECT 漏掉一列各报什么,以及和 1.x 的区别,在 列与值 里用实验对比。
ModelTrait 的实现是三个按列的 match:get 返回 self.field.clone().into(),get_value_type 返回 <字段类型 as ValueType>::array_type(),try_set 用 ValueType::try_from(v) 写回字段。
第三段:ActiveModel
ActiveModel 和 Model 字段一一对应,每个包一层 ActiveValue:
pub struct ActiveModel {
/// Generated by sea-orm-macros
pub id: sea_orm::ActiveValue<i32>,
/// Generated by sea-orm-macros
pub title: sea_orm::ActiveValue<String>,
/// Generated by sea-orm-macros
pub body: sea_orm::ActiveValue<Option<String>>,
}
// ...
impl std::default::Default for ActiveModel {
fn default() -> Self {
<Self as sea_orm::ActiveModelBehavior>::new()
}
}
impl std::convert::From<Model> for ActiveModel {
fn from(m: Model) -> Self {
Self {
id: sea_orm::ActiveValue::Unchanged(m.id),
title: sea_orm::ActiveValue::Unchanged(m.title),
body: sea_orm::ActiveValue::Unchanged(m.body),
}
}
}
Default 转给 ActiveModelBehavior::new(),这就是为什么每个实体都要写那行 impl ActiveModelBehavior for ActiveModel {}:宏生成的代码依赖它,宏自己却不生成它,留给你覆盖 before_save 之类的钩子。From<Model> 把每个字段标成 Unchanged。三种状态和写入路径在 ActiveValue。
#[sea_orm::model] 改写了什么
sea_orm::model 是 sea_orm_model 这个属性宏的重导出(src/lib.rs 里 sea_orm_model as model)。它跑在 derive 之前,拿到整个 struct,交给 expand_sea_orm_model 输出两个 struct:
let model_ex = format_ident!("{model}Ex");
for attr in &mut inherited_model_ex_attrs {
// ...
list.parse_nested_meta(|meta| {
if meta.path.is_ident("Eq") {
// Nested relations may contain non-`Eq` fields, so `Eq` cannot be forwarded safely.
} else if meta.path.is_ident("DeriveEntityModel") {
// ModelEx uses its own derive macros.
new_list.push(parse_quote!(DeriveModelEx));
new_list.push(parse_quote!(DeriveActiveModelEx));
} else {
new_list.push(meta.path);
}
Ok(())
})?;
// ...
}
// ...
Ok(quote! {
#(#model_attrs)*
#[sea_orm(model_ex)]
#vis struct #model {
#(#model_fields),*
}
#(#inherited_model_ex_attrs)*
#(#model_ex_attrs)*
#compact_model
#[allow(
clippy::derive_partial_eq_without_eq,
reason = "Nested relations may contain non-`Eq` fields, so `ModelEx` does not inherit `Eq`"
)]
#vis struct #model_ex #all_fields
})
Model只保留标量字段(类型是HasOne<..>/HasMany<..>/BelongsTo<..>的关系字段被挑出去),加上#[sea_orm(model_ex)]标记,然后照常走DeriveEntityModel。ModelEx保留全部字段,derive 列表里的DeriveEntityModel被换成DeriveModelEx和DeriveActiveModelEx,Eq被去掉:关系字段里可能有不能Eq的东西。
DeriveEntityModel 看到 model_ex 标记,只改两处:不再生成 impl Model { fn into_ex(self) -> Self },并给 Entity 加 #[sea_orm(model_ex = ModelEx, active_model_ex = ActiveModelEx)],让 DeriveEntity 把那两个关联类型指过去。把两份展开的前 900 行做 diff,差别只有这些:
7a8
> #[sea_orm(model_ex)]
61,66d61
< impl Model {
< /// Generated by sea-orm-macros
< pub fn into_ex(self) -> Self {
< self
< }
< }
280a276
> #[sea_orm(model_ex = ModelEx, active_model_ex = ActiveModelEx)]
311c307
< type ModelEx = Model;
---
> type ModelEx = ModelEx;
313c309
< type ActiveModelEx = ActiveModel;
---
> type ActiveModelEx = ActiveModelEx;
903a900,904
> #[sea_orm(table_name = "post")]
> #[allow(
> clippy::derive_partial_eq_without_eq,
> reason = "Nested relations may contain non-`Eq` fields, so `ModelEx` does not inherit `Eq`"
> )]
Column、Entity、PrimaryKey、FromQueryResult、ModelTrait、ActiveModel 逐行相同。dense 格式多出来的 1100 多行全部追加在后面,由 ModelEx 上的 DeriveModelEx 和 DeriveActiveModelEx 生成:
| 生成项 | 展开里的样子 | 给谁用 |
|---|---|---|
ModelEx | Model 的全部字段加关系字段;From<Model>(关系字段 Default::default())、From<ModelEx> for Model、两者互相 PartialEq;Model::into_ex() 改成返回 ModelEx | 带着关联数据的结果 |
TypedColumn 和 COLUMN | pub struct TypedColumn { pub id: sea_orm::NumericColumn<Entity>, pub title: sea_orm::StringColumn<Entity>, pub body: sea_orm::StringColumnNullable<Entity> },外加模块级 pub const COLUMN 和 impl Entity { pub const COLUMN: TypedColumn = COLUMN; } | 强类型条件,见 列与值 |
Relation、Related 实现 | 从关系字段生成;这里没有关系字段,于是是 pub enum Relation {},def() 只有一个 panic!("No RelationDef for Relation") 分支 | 见 关系 |
RelatedEntity | 每个关联实体一个变体,这里为空 | 关联实体的枚举 |
EntityLoader | Entity::load() 返回它,one / all 返回 ModelEx;Entity::REVERSE | 见 Entity Loader |
find_by_<列> / delete_by_<列> | 每个 unique 字段(或 unique_key 组)一对 | 按唯一键查找 |
ActiveModelEx | 普通列同 ActiveModel(ActiveValue),关系字段换成 ActiveBelongsTo / ActiveHasOne / ActiveHasMany;带 insert / update 等方法;ActiveModel::builder()、ActiveModel::into_ex();Model::cascade_delete | 嵌套写入,见 ActiveModelEx |
find_by_<列> 用一个 #[sea_orm(unique)] pub email: String 的实体试一下(src/bin/unique_find.rs),user::Entity::find_by_email("a@x.com") 生成:
SELECT "user"."id", "user"."email" FROM "user" WHERE "user"."email" = $1
强类型列只在 dense 格式下有。对展开写法的实体写 post_plain::COLUMN.id(src/bin/plain_no_column.rs):
error[E0425]: cannot find value `COLUMN` in module `post_plain`
--> src/bin/plain_no_column.rs:4:23
|
4 | let _ = post_plain::COLUMN.id;
| ^^^^^^ not found in `post_plain`
TypedColumn 每个字段用哪种包装,由 column_type_wrapper 决定:写了 column_type 就按它(String / Text 是 StringColumn,各种整数、浮点、Decimal、Money 是 NumericColumn……),否则看字段类型的名字:bool、String、Vec<u8>、Uuid、Json、各整数和浮点、名字里含 DateTime / Timestamp / Date / Time 的类型各有对应;字段是 Option<..> 时 String 和数字用 ...Nullable 版本。认不出来的类型(比如 DeriveValueType 的 newtype、DeriveActiveEnum 的 enum)退回普通的 Column,COLUMN.x 就是 Column::X,没有额外检查。
问: #[sea_orm::model] 写在 #[derive(...)] 下面行不行?反正都是同一个 struct 上的属性。
答: 不行。换个顺序(#[derive(Clone, Debug, PartialEq, Eq, DeriveEntityModel)] 在上,#[sea_orm::model] 在下),编译报:
error[E0425]: cannot find type `Relation` in this scope
--> src/bin/attr_order.rs:4:41
|
4 | #[derive(Clone, Debug, PartialEq, Eq, DeriveEntityModel)]
| ^^^^^^^^^^^^^^^^^
|
= note: this error originates in the derive macro `sea_orm::prelude::DeriveEntity` (in Nightly builds, run with -Z macro-backtrace for more info)
报错来自 DeriveEntity:它生成的 type Relation = Relation; 找不到类型。也就是说 DeriveEntityModel 走了展开写法的路径,要求你自己写 Relation。dense 格式的 Relation、ModelEx 都是靠属性宏先改写 struct(加上 model_ex 标记、拆出 ModelEx)才生成的,属性必须写在 derive 之前。
两种写法,同一张表,同样的 SQL
前 900 行逐行相同已经说明查询和写入用的是同一套实现。直接比一下两种实体 build 出来的 Statement(src/bin/same_sql.rs):
// The dense and the expanded entity map the same table: compare what they build.
fn main() {
let a = post::Entity::find_by_id(1).build(DbBackend::Postgres);
let b = post_plain::Entity::find_by_id(1).build(DbBackend::Postgres);
println!("find_by_id same: {} | {}", a == b, a.sql);
let a = post::Entity::insert(post::ActiveModel { title: Set("t".into()), ..Default::default() })
.build(DbBackend::Postgres);
let b = post_plain::Entity::insert(post_plain::ActiveModel { title: Set("t".into()), ..Default::default() })
.build(DbBackend::Postgres);
println!("insert same: {} | {}", a == b, a.sql);
}
find_by_id same: true | SELECT "post"."id", "post"."title", "post"."body" FROM "post" WHERE "post"."id" = $1
insert same: true | INSERT INTO "post" ("title") VALUES ($1)
Statement 的相等比较包括 SQL 文本、参数值和后端。(insert 的 build 里还没有 RETURNING,它是执行时加上的,见 insert。)dense 格式的代价是编译时间和生成代码量:同一个三列实体,展开结果从 1037 行变成 2206 行,其中 EntityLoader 和 ActiveModelEx 占了大半。本站的实体都是展开写法,迁到 2.0 要不要改,在 升级到 2.0 讨论。
动脑筋答案
Id -> "id"
Type -> "type"
CreatedAt -> "created_at"
Name -> "name"
DisplayName -> "Display Name"
createdAt的列名是created_at,不是createdAt。字段名转成变体名CreatedAt,再转回 snake_case 是created_at,和字段名不同,于是宏把"createdAt".to_snake_case()当列名。如果表里那一列真叫"createdAt",要写column_name = "createdAt"。name_的列名是name:变体名Name转回来是name,和name_不同,于是取"name_".to_snake_case(),尾部的下划线被去掉了(生成代码里的注释写的就是「to_snake_casewas used to trim prefix and tailing underscore」)。r#type先去掉r#前缀,变体是Type、列名是type。column_name原样使用,可以带空格和大写,sea-query 写 SQL 时会加引号。
小结
DeriveEntityModel要求 struct 叫Model,输出三段:Column/Entity/PrimaryKey(借内层的DeriveColumn、DeriveEntity、DerivePrimaryKey),FromQueryResult/ModelTrait,ActiveModel一族。impl ActiveModelBehavior必须自己写,Default依赖它。- 列类型来自字段的 Rust 类型,
Option<T>按文本剥掉并标nullable;column_name缺省时由字段名推出,createdAt会变成created_at。 nullable、unique、default_value等只改ColumnDef;select_as/save_as在 SQL 里加CAST;ignore的字段解码时填Default::default()。- 生成的
from_query_result先把每列读成Option,全空返回TryGetError::Null,缺值报Missing value for column。 #[sea_orm::model]必须写在 derive 之前。它把 struct 拆成Model和ModelEx,DeriveEntityModel的输出只差into_ex和两个关联类型;追加的是ModelEx、TypedColumn/COLUMN、生成的Relation、RelatedEntity、EntityLoader、find_by_<unique>、ActiveModelEx和builder()。- 强类型列
COLUMN只有 dense 格式才有;两种写法对同一张表build出的Statement完全相等。