Rust in Everything
基于 sea-orm 2.0.4

DeriveEntityModel 与 dense 格式

主线位置:定义主线本身。你写一个 Model struct,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
}
段展开函数生成展开写法里的行
1expand_derive_entity_modelColumn 枚举及 ColumnTrait;Entity 及 EntityName;PrimaryKey 及 PrimaryKeyTrait61–486
2expand_derive_model(impl_from_query_result、impl_model_trait)FromQueryResult for Model、ModelTrait for Model487–645
3expand_derive_active_modelActiveModel struct、ActiveModelTrait、From<Model>、TryFrom<ActiveModel> for Model646–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 = boolauto_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 当列名。

❗IMPORTANT
动脑筋:这几个字段在 SQL 里叫什么?

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 生成:

生成项展开里的样子给谁用
ModelExModel 的全部字段加关系字段;From<Model>(关系字段 Default::default())、From<ModelEx> for Model、两者互相 PartialEq;Model::into_ex() 改成返回 ModelEx带着关联数据的结果
TypedColumn 和 COLUMNpub 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每个关联实体一个变体,这里为空关联实体的枚举
EntityLoaderEntity::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,没有额外检查。

📝NOTE
没有蠢问题

问: #[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_case was 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 完全相等。

💬 关联讨论

发起讨论
加载中...