实体的几个 trait
主线位置:定义主线的终点,也是另外两条主线的起点。查询主线的第一步
Entity::find_by_id(42)是EntityTrait的默认方法,它靠PrimaryKey找到主键列、靠Column写出WHERE、靠EntityName写出FROM;最后一步Model::from_query_result是FromQueryResult的方法。这一页只讲这些 trait 长什么样、为什么这样分;它们的实现由宏生成,逐段看在 DeriveEntityModel 与 dense 格式;列上的条件方法和取值在 列与值。
flowchart TB iden["Iden / IdenStatic"] --> en["EntityName 表名"] iden --> ct[ColumnTrait] iden --> pt[PrimaryKeyTrait] en --> et[EntityTrait] et -->|Model| mt["ModelTrait + FromQueryResult"] et -->|Column| ct et -->|PrimaryKey| pt et -->|ActiveModel| am[ActiveModelBehavior] et -->|Relation| rt[RelationTrait] pt -->|PrimaryKeyToColumn| ct ct -->|EntityName| en
本页的实体是下面这个 post(2.0 的 dense 写法;1.x 的展开写法生成的 trait 实现与它逐行相同,见下一页)。实验在 M2 Max、macOS、rustc 1.99.0、Postgres 16.14 上跑,依赖 sea-orm = "=2.0.4"(sqlx-postgres、runtime-tokio-rustls、macros、with-chrono、with-json、with-uuid、postgres-array),表建在自己的 schema 里:
pub 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 {}
}
类型就是表
宏生成的 Entity 是一个单元结构体:
pub struct Entity;
它不持有任何数据,size_of::<post::Entity>() 是 0。表名、schema 名都不是字段,而是 EntityName 的方法返回的常量:
pub trait IdenStatic: Iden + Copy + Debug + Send + Sync + 'static {
/// Method to call to get the static string identity
fn as_str(&self) -> &'static str;
}
// ...
pub trait EntityName: IdenStatic + Default {
/// Method to get the name for the schema, defaults to [Option::None] if not set
fn schema_name(&self) -> Option<&str> {
None
}
/// Method to get the comment for the schema, defaults to [Option::None] if not set
fn comment(&self) -> Option<&str> {
None
}
/// Get the name of the table
fn table_name(&self) -> &'static str;
/// Get the [TableRef] from invoking the `self.schema_name()`
fn table_ref(&self) -> TableRef {
match self.schema_name() {
Some(schema) => (schema.to_owned(), self.into_iden()).into_table_ref(),
None => self.into_table_ref(),
}
}
}
几个约束各有用处:
IdenStatic(定义)要求实现 sea-query 的Iden,于是post::Entity本身就能当表名交给 sea-query;as_str返回&'static str,宏把它实现成table_name()。Copy和Default:泛型代码需要一个表的值时,直接Self::default()造一个,不用谁传进来。EntityTrait::update_many就是Update::many(Self::default()),ColumnTrait::entity_name是SeaRc::new(Self::EntityName::default())。单元结构体的default()什么也不做。- 关联函数不收
self:find、insert都写成post::Entity::find()。实体的「值」只在需要一个标识符时出现。
Entity 和 Column 是普通的 sea-query 标识符,可以绕过 sea-orm 直接拼语句(src/bin/traits_demo.rs,节选):
let sql = Query::select().column(post::Column::Title).from(post::Entity).to_string(PostgresQueryBuilder);
println!("sea-query: {sql}");
sea-query: SELECT "title" FROM "post"
sea-orm 自己拼语句时用的是 ColumnTrait::as_column_ref,它返回 (表, 列) 一对,所以 Select 里的列都带表名前缀("post"."title")。sea-query 的语句树和这些标识符怎么被写成 SQL,见 语句树。
Iden 在 sea-query 1.0 里是一个字符串
sea-query 的 Iden 只有一个必须实现的方法 unquoted,另外两个有默认实现:
pub trait Iden {
// ...
fn quoted(&self) -> Cow<'static, str> {
Cow::Owned(self.to_string())
}
// ...
fn to_string(&self) -> String {
self.unquoted().to_owned()
}
// ...
fn unquoted(&self) -> &str;
}
存进语句树的是 DynIden,它就是 quoted() 的结果:pub struct DynIden(pub(crate) Cow<'static, str>)。Cow::Borrowed 表示一个静态的、不需要转义的名字,Cow::Owned 总会被转义。宏为 Column 和 PrimaryKey 生成的 Iden 在所有列名都是不需要转义的简单名字时(is_static_iden)重写 quoted,返回 Cow::Borrowed(as_str());为 Entity 生成的只写了 unquoted,quoted 走默认实现。验证一下(src/bin/iden_cow.rs):
let kind = |c: Cow<'static, str>| if matches!(c, Cow::Borrowed(_)) { "Borrowed" } else { "Owned" };
println!("Entity -> {}", kind(post::Entity.into_iden().inner()));
println!("Column::Title -> {}", kind(post::Column::Title.into_iden().inner()));
println!("PrimaryKey::Id -> {}", kind(post::PrimaryKey::Id.into_iden().inner()));
Entity -> Owned
Column::Title -> Borrowed
PrimaryKey::Id -> Borrowed
列名不分配,表名每转一次 DynIden 分配一个 String。as_column_ref 每次都调 entity_name(),所以一条 SELECT 里每个带表名前缀的列引用都有一次这样的分配。和一次网络往返比,这点开销可以忽略,但它说明 Cow 这一层确实区分了两种情况。
DynIden 以前是一个 Rc<dyn Iden>sea-query 0.32(本站用的 0.32.7)里 pub type DynIden = SeaRc<dyn Iden>;,SeaRc 按 thread-safe feature 是 Rc 或 Arc。标识符以 trait 对象的形式存进语句树,直到语句被序列化成 SQL 时才调用它渲染。sea-query#909 把它改成了今天的样子,CHANGELOG 记在 1.0.0 之下:「Previously, DynIden is an alias to SeaRc<dyn Iden>, and is lazily rendered. Now, it's an Cow<'static, str>, and is eagerly rendered. SeaRc is no longer an alias to Rc / Arc, now is only a unit struct. As such, Send / Sync is no longer needed.」PR 的说明是「Arc, vtable tricks are removed」,并指出对 sea-orm 影响不大:「For majority of usecases in SeaORM though, since they're 'static str anyway, there should be no difference.」
sea-orm 2.0 的 as_column_ref 里还写着 SeaRc::new(*self),但 SeaRc 现在是一个单元结构体,SeaRc::new 是「A legacy method, kept for compatibility」,直接返回 DynIden:当场调用 quoted(),然后丢掉原来的对象。
EntityTrait:七个关联类型
EntityTrait 是把一个实体的各部分串起来的地方:
pub trait EntityTrait: EntityName {
#[allow(missing_docs)]
type Model: ModelTrait<Entity = Self> + FromQueryResult;
#[allow(missing_docs)]
type ModelEx: ModelTrait<Entity = Self>;
#[allow(missing_docs)]
type ActiveModel: ActiveModelBehavior<Entity = Self>;
// ...
type ActiveModelEx: ActiveModelTrait<Entity = Self>;
#[allow(missing_docs)]
type Column: ColumnTrait;
#[allow(missing_docs)]
type Relation: RelationTrait;
#[allow(missing_docs)]
type PrimaryKey: PrimaryKeyTrait + PrimaryKeyToColumn<Column = Self::Column>;
每个关联类型都带着 Entity = Self 或 Column = Self::Column 指回来,编译器因此能检查「这个 Model 属于这张表」「这个主键的列是这张表的列」。find_by_id 里从 PrimaryKey 拿到的列,类型就是 Self::Column,可以直接 col.eq(v)。
ModelEx 和 ActiveModelEx 是 2.0 新加的,给 dense 格式里「带着关联数据的 Model」用(posts: HasMany<...> 这样的字段放不进 Model)。不用 dense 格式时,宏让它们就是 Model 和 ActiveModel,展开结果里是:
impl sea_orm::entity::EntityTrait for Entity {
type Model = Model;
type ModelEx = Model;
type ActiveModel = ActiveModel;
type ActiveModelEx = ActiveModel;
type Column = Column;
type PrimaryKey = PrimaryKey;
type Relation = Relation;
}
dense 格式下这两行换成 ModelEx / ActiveModelEx,其余不变(下一页有对比)。Relation 和 RelationTrait 在 关系。
trait 里没有一个方法需要实现者写,查询和写入的入口全是默认方法:
| 方法 | 返回 | 做什么 |
|---|---|---|
find() | Select<Self> | Select::new():选出全部列,FROM 表名 |
find_by_id(v) | Select<Self> | find() 再对每个主键列 filter(col.eq(v)) |
insert(am) / insert_many(iter) | Insert<A> / InsertMany<A> | 包一层,见 insert |
update(am) / update_many() | UpdateOne<A> / UpdateMany<Self> | 见 update 与 delete |
delete(am) / delete_many() | DeleteOne<Self> / DeleteMany<Self> | 同上 |
delete_by_id(v) | ValidatedDeleteOne<Self> | 造一个只设了主键的 ActiveModel 再 Delete::one |
belongs_to / has_one / has_many / has_many_via | RelationBuilder<Self, R> | 手写 Relation 时用,见 关系 |
find_by_id:主键的类型就是参数的类型
fn find_by_id<T>(values: T) -> Select<Self>
where
T: Into<<Self::PrimaryKey as PrimaryKeyTrait>::ValueType>,
{
let mut select = Self::find();
let mut keys = Self::PrimaryKey::iter();
for v in values.into().into_value_tuple() {
if let Some(key) = keys.next() {
let col = key.into_column();
select = select.filter(col.eq(v));
} else {
unreachable!("primary key arity mismatch");
}
}
select
}
参数先 into() 成主键的 ValueType(单列主键就是那一列的 Rust 类型,复合主键是元组),再 into_value_tuple() 拆成一串 Value,和 PrimaryKey::iter() 一一配对。ValueType 是一个关联类型,所以参数个数和类型都在编译期检查:给单列 i32 主键传元组或字符串,rustc 直接拒绝(src/bin/pk_arity.rs):
fn main() {
let _ = post::Entity::find_by_id((1, 2));
let _ = post::Entity::find_by_id("1");
}
error[E0277]: the trait bound `i32: From<({integer}, {integer})>` is not satisfied
--> src/bin/pk_arity.rs:5:36
|
5 | let _ = post::Entity::find_by_id((1, 2));
| ------------------------ ^^^^^^ the trait `From<({integer}, {integer})>` is not implemented for `i32`
| |
| required by a bound introduced by this call
...
= note: required for `({integer}, {integer})` to implement `Into<i32>`
note: required by a bound in `find_by_id`
--> ~/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/sea-orm-2.0.4/src/entity/base_entity.rs:323:12
...
error[E0277]: the trait bound `i32: From<&str>` is not satisfied
--> src/bin/pk_arity.rs:6:36
|
6 | let _ = post::Entity::find_by_id("1");
| ------------------------ ^^^ the trait `From<&str>` is not implemented for `i32`
...
unreachable!("primary key arity mismatch") 因此真的走不到:元组的长度和 PrimaryKey 的变体数由同一个宏从同一组 #[sea_orm(primary_key)] 字段生成。这个检查比列上的 Column::Id.eq(..) 严格得多,后者收任何 Into<Value>,见 列与值。
ColumnTrait:一列知道什么
ColumnTrait 的骨架只有几项,其余几十个方法都是在 as_column_ref 之上造 Expr 的默认方法:
pub trait ColumnTrait: IdenStatic + Iterable + FromStr {
#[allow(missing_docs)]
type EntityName: EntityName;
/// Get the column definition with SQL attributes
fn def(&self) -> ColumnDef;
// ...
fn enum_type_name(&self) -> Option<&'static str> {
None
}
/// Get the name of the entity the column belongs to
fn entity_name(&self) -> DynIden {
SeaRc::new(Self::EntityName::default())
}
/// Get the table.column reference
fn as_column_ref(&self) -> (DynIden, DynIden) {
(self.entity_name(), SeaRc::new(*self))
}
Iterable(就是 strum 的IntoEnumIterator):Column::iter()按字段顺序给出每一列,find()的SELECT列表就是这样来的。FromStr:从列名找回变体,给按字符串排序、过滤的场合用。生成的实现接受三种写法:变体名的 snake_case、lowerCamelCase,以及column_name指定的名字。def()返回ColumnDef(列类型、可空、唯一、索引、默认值),主要给建表和 schema 同步用;查询时eq这类方法也会调它,只为判断要不要给值加类型转换(enum 列、JSON 数组)。细节在 列与值。
let cols: Vec<&str> = post::Column::iter().map(|c| c.as_str()).collect();
println!("Column::iter() = {cols:?}");
println!("from_str(\"title\") = {:?}, from_str(\"Title\") = {:?}", post::Column::from_str("title"), post::Column::from_str("Title"));
Column::iter() = ["id", "title", "body"]
from_str("title") = Ok(Title), from_str("Title") = Err(ColumnFromStrErr("Title"))
Column 枚举的值不带表的信息,as_column_ref 靠关联类型 EntityName 和它的 Default 现造一个表名。这是 EntityName: Default 的另一个用处。
PrimaryKeyTrait 与复合主键
PrimaryKeyTrait 有一个关联类型和一个方法:
pub trait PrimaryKeyTrait: IdenStatic + Iterable {
#[allow(missing_docs)]
type ValueType: Sized
+ Send
+ Debug
+ PartialEq
+ IntoValueTuple
+ FromValueTuple
+ TryGetableMany
+ TryFromU64
+ PrimaryKeyArity;
/// Method to determine if Primary Key is not auto_increment
fn auto_increment() -> bool;
}
ValueType 的约束各有使用者:IntoValueTuple 给 find_by_id / delete_by_id 拆值;插入之后,TryGetableMany 把 RETURNING 回来的主键列读成 ValueType,主键是自己 Set 的、不用读回时,FromValueTuple 把那串 Value 直接变回去;TryFromU64 给不支持 RETURNING 的后端从 last_insert_id 造主键;PrimaryKeyArity 给出列数。
PrimaryKeyArity 是一个常量 ARITY。任何 TryGetable 的单个类型是 1(blanket impl),元组由宏实现到 12 个元素。primary_key_identity() 和 ModelTrait::get_primary_key_value() 都按 ARITY 在 match 里分 1、2、3 和更多,产出 Identity 的 Unary / Binary / Ternary / Many:常见的一到三列不分配 Vec。
PrimaryKeyToColumn 把主键变体映射到同名的列变体:PrimaryKey 只是 Column 的一个子集,单独成一个枚举是为了让 PrimaryKey::iter() 只遍历主键列。
复合主键就是两个字段都标 primary_key,ValueType 是元组(src/bin/traits_demo.rs,节选):
entity!(tag_link, "tag_link", {
#[sea_orm(primary_key)] pub post_id: i32,
#[sea_orm(primary_key)] pub tag_id: i32,
});
let stmt = tag_link::Entity::find_by_id((1, 2)).build(DbBackend::Postgres);
println!("find_by_id((1, 2)): {} {:?}", stmt.sql, stmt.values.map(|v| v.0).unwrap_or_default());
find_by_id((1, 2)): SELECT "tag_link"."post_id", "tag_link"."tag_id" FROM "tag_link" WHERE "tag_link"."post_id" = $1 AND "tag_link"."tag_id" = $2 [Int(Some(1)), Int(Some(2))]
(entity! 是实验程序里的一个小宏,展开成和上面 post 同样形状的 #[sea_orm::model] 模块。)这里打印的是 build 出来的 Statement:SQL 里是 $N 占位符,值另放在 values 里,和交给 sqlx 的完全一样。
auto_increment() 告诉写入路径「主键 NotSet 时是否由数据库生成」。默认值由宏推断,可以用 #[sea_orm(auto_increment = false)] 覆盖。
auto_increment() 是 true?都不写 auto_increment 属性,除了第四个(M2 Max、macOS、rustc 1.99.0,src/bin/traits_demo.rs):
entity!(by_uuid, "by_uuid", { #[sea_orm(primary_key)] pub id: Uuid, pub n: i32 });
entity!(by_code, "by_code", { #[sea_orm(primary_key)] pub code: String, pub n: i32 });
entity!(by_i64_manual, "by_i64_manual", { #[sea_orm(primary_key, auto_increment = false)] pub id: i64 });
// tag_link: (post_id: i32, tag_id: i32) 复合主键,见上文
println!("auto_increment: i32={} Uuid={} String={} i64(manual)={} (i32,i32)={}",
<post::PrimaryKey as PrimaryKeyTrait>::auto_increment(),
<by_uuid::PrimaryKey as PrimaryKeyTrait>::auto_increment(),
<by_code::PrimaryKey as PrimaryKeyTrait>::auto_increment(),
<by_i64_manual::PrimaryKey as PrimaryKeyTrait>::auto_increment(),
<tag_link::PrimaryKey as PrimaryKeyTrait>::auto_increment(),
);
ModelTrait 与 FromQueryResult
ModelTrait 让一个 Model 能按 Column 存取字段,值的类型统一是 sea-query 的 Value:
pub trait ModelTrait: Clone + Send + Debug {
#[allow(missing_docs)]
type Entity: EntityTrait;
/// Get the [Value] of a column from a Model
fn get(&self, c: <Self::Entity as EntityTrait>::Column) -> Value;
// ...
fn get_value_type(c: <Self::Entity as EntityTrait>::Column) -> ArrayType;
// ...
fn try_set(&mut self, c: <Self::Entity as EntityTrait>::Column, v: Value) -> Result<(), DbErr>;
get 是一个大 match,每个分支 self.field.clone().into();try_set 反过来用 ValueType::try_from(v),类型不符返回 DbErr::Type。按列动态存取的地方都靠它:get_primary_key_value 逐个 get 主键列;find_related(..) 生成的 belongs_to 过滤是 col.eq(model.get(col)),从当前这一行取关联列的值。find_related / find_linked 也是这里的默认方法,见 关系。
FromQueryResult 是查询主线的最后一步,Selector::one 拿到 QueryResult 后调它:
pub trait FromQueryResult: Sized {
// ...
fn from_query_result(res: &QueryResult, pre: &str) -> Result<Self, DbErr>;
// ...
fn from_query_result_optional(res: &QueryResult, pre: &str) -> Result<Option<Self>, DbErr> {
Ok(Self::from_query_result(res, pre).ok())
// ...
}
// ...
fn from_query_result_nullable(res: &QueryResult, pre: &str) -> Result<Self, TryGetError> {
Self::from_query_result(res, pre).map_err(TryGetError::DbErr)
}
pre 是列名前缀:find_also_related 把两张表的列放进同一行,用 A_、B_ 这样的别名区分,读第二个 Model 时传 pre = "B_"(见 join 与关联查询)。from_query_result_nullable 把「NULL」和「真的出错」分开返回,Option<T> 的实现把 TryGetError::Null 变成 Ok(None),LEFT JOIN 没匹配上的那一侧就靠它变成 None。宏为 Model 生成的实现长什么样在下一页,一列 NULL 落进非 Option 字段时报什么在 列与值。
动脑筋答案
auto_increment: i32=true Uuid=false String=false i64(manual)=false (i32,i32)=false
i32:只有一个主键字段,没写属性,默认true。Uuid和String:宏里有一个常量NOT_AUTO_INCRE_TYPE_SUFFIX= ["String", "Uuid"],主键字段的类型名(去掉Option<>后的文本)以其中之一结尾时,没写属性也推断为false。判断是按类型的字面文本做的,一个叫UserUuid的 newtype 也会命中。i64写了auto_increment = false,照办。- 复合主键:生成代码是
value && primary_keys.len() == 1,只要不是单列,就一定是false。宏还会拒绝在复合主键上写auto_increment(报auto_increment cannot be used on composite primary_key)。
小结
Entity是零大小的单元结构体,表名、schema 名是EntityName方法返回的常量;Default + Copy让泛型代码随时造一个表的值。Entity和Column都是 sea-query 的Iden,能直接用在 sea-query 语句里。- sea-query 1.0 的
DynIden是Cow<'static, str>,不再是Rc<dyn Iden>;列名是Borrowed,宏生成的Entity的quoted走默认实现,每次是Owned。 EntityTrait用七个关联类型串起整个实体,2.0 新增的ModelEx/ActiveModelEx在非 dense 写法下就是Model/ActiveModel;find、insert、delete_by_id等全是默认方法。find_by_id的参数要Into主键的ValueType,单列是那一列的类型,复合主键是元组,个数和类型都在编译期检查。PrimaryKeyArity给出主键列数,Identity和ValueTuple对一到三列不分配;auto_increment对String/Uuid结尾的类型和复合主键推断为false。ModelTrait按Column存取Value;FromQueryResult带列名前缀pre,Option<T>的实现把 NULL 变成None。