关系
主线位置:定义主线的最后一段。
DeriveEntityModel生成了表和列,这一页讲表和表之间的连线:Relation枚举的每个分支给出一个RelationDef,Related<R>说「从这个实体走到R用哪条(或哪两条)连线」,Linked描述一条多跳或自引用的路径。join、find_related、Loader、建外键的 DDL 都只读RelationDef。这些查询怎么执行在 join 与 find_related 和 Loader 与 N+1,本页只看每种关系变成什么样的JOIN ... ON。
flowchart TB dense["dense 字段 BelongsTo 等"] -->|"sea_orm::model"| attrs["Relation 枚举 + 属性"] legacy["1.x 手写 Relation"] --> attrs attrs -->|DeriveRelation| builder["Entity::belongs_to 等"] builder --> rdef[RelationDef] rdef --> cond["ON 条件"] rdef --> fk["外键 DDL"] rdef --> rel["Related to / via"] rdef --> lnk["Linked 一串 def"] cond --> join["join / join_rev"]
实验 crate 依赖 sea-orm =2.0.4(sqlx-postgres、runtime-tokio-rustls、macros、with-json、postgres-array),平台 M2 Max、macOS、rustc 1.99.0、Postgres 16.14。实体是一组小表:author、post(author_id 指向 author)、tag、中间表 post_tag、自引用的 staff(reports_to_id 指向本表)。同一组表写了两遍:legacy 模块是 1.x 的展开写法,dense 模块是 2.0 的 #[sea_orm::model]。
RelationDef:一条连线的全部信息
RelationDef 是一个普通的公开结构体:
pub struct RelationDef {
/// Cardinality of the relation.
pub rel_type: RelationType,
/// Table at the FK-owning end of the relation.
pub from_tbl: TableRef,
/// Table being pointed to by the foreign key.
pub to_tbl: TableRef,
/// FK column(s) on [`from_tbl`](Self::from_tbl).
pub from_col: Identity,
/// Referenced column(s) on [`to_tbl`](Self::to_tbl) (usually the primary key).
pub to_col: Identity,
/// `true` if `Self` is the FK-owning side (i.e. holds `from_col`).
pub is_owner: bool,
/// Skip emitting a `FOREIGN KEY` constraint when this relation is used
/// to generate schema DDL.
pub skip_fk: bool,
/// `ON DELETE` action for the foreign key.
pub on_delete: Option<ForeignKeyAction>,
/// `ON UPDATE` action for the foreign key.
pub on_update: Option<ForeignKeyAction>,
/// Extra predicate to AND/OR into the join's `ON` clause; see
/// [`RelationDef::on_condition`].
#[debug("{}", on_condition.is_some())]
pub on_condition: Option<Arc<dyn Fn(DynIden, DynIden) -> Condition + Send + Sync>>,
/// Name of the foreign-key constraint to emit in DDL.
pub fk_name: Option<String>,
/// How [`on_condition`](Self::on_condition) is combined with the
/// column equality predicate (`All` = AND, `Any` = OR).
pub condition_type: ConditionType,
}
字段按用途分四组:
| 组 | 字段 | 谁读 |
|---|---|---|
| 连线本身 | from_tbl / from_col、to_tbl / to_col | 生成 ON from.col = to.col |
| 方向和基数 | rel_type(RelationType 只有 HasOne、HasMany)、is_owner | Loader、find_related 判断该收集哪一边的键 |
| 附加条件 | on_condition、condition_type | 往 ON 里再并一个条件 |
| 外键 DDL | skip_fk、on_delete、on_update、fk_name | From<RelationDef> for TableForeignKey,schema-sync 建表时用 |
注意没有 BelongsTo 这种 RelationType。belongs_to 只是「HasOne,且 is_owner = false」:外键在 from_tbl 上,from_tbl 这一边不是被指向的一方。字段注释里的「true if Self is the FK-owning side」容易读反,下面的实测输出里,持有外键的 post -> author 是 is_owner: false,被指向的 author -> post 是 is_owner: true。is_owner 的「owner」指的是被引用的那张表。
on_condition 是一个闭包,收两个参数:ON 左右两张表的名字(有别名时是别名)。所以它能写「右边那张表的某一列大于 10」而不用写死表名。Debug 打不出闭包,derive_more::Debug 的 #[debug(...)] 只打印它有没有设置。
从 RelationDef 到 ON 条件
join 不需要单独的「join 描述」,RelationDef 直接实现了 From<RelationDef> for Condition:
impl From<RelationDef> for Condition {
fn from(mut rel: RelationDef) -> Condition {
// Use table alias (if any) to construct the join condition
let from_tbl = match rel.from_tbl.sea_orm_table_alias() {
Some(alias) => alias,
None => rel.from_tbl.sea_orm_table(),
};
let to_tbl = match rel.to_tbl.sea_orm_table_alias() {
Some(alias) => alias,
None => rel.to_tbl.sea_orm_table(),
};
let owner_keys = rel.from_col;
let foreign_keys = rel.to_col;
let mut condition = match rel.condition_type {
ConditionType::All => Condition::all(),
ConditionType::Any => Condition::any(),
};
condition = condition.add(join_tbl_on_condition(
from_tbl.clone(),
to_tbl.clone(),
owner_keys,
foreign_keys,
));
if let Some(f) = rel.on_condition.take() {
condition = condition.add(f(from_tbl.clone(), to_tbl.clone()));
}
condition
}
}
join_tbl_on_condition 把 from_col 和 to_col 一一配对写成 from_tbl.a = to_tbl.b,复合键就是几个等式 AND 起来。表有别名时用别名,这是自引用能工作的前提(后面讲)。QuerySelect 的两个方法只差在 JOIN 后面跟哪张表(join / join_rev):
fn join(mut self, join: JoinType, rel: RelationDef) -> Self {
self.query().join(join, rel.to_tbl.clone(), rel);
self
}
// ...
fn join_rev(mut self, join: JoinType, rel: RelationDef) -> Self {
self.query().join(join, rel.from_tbl.clone(), rel);
self
}
ON 条件是同一个,join_rev 只是把 from_tbl 接进来。实测(QueryTrait::build(DbBackend::Postgres).to_string(),select_only() 去掉了列,所以 SELECT 后面是空的;这里没有参数,to_string 和实际发出的 SQL 一样):
join(Inner, post::Relation::Author.def()):
SELECT FROM "post" INNER JOIN "author" ON "post"."author_id" = "author"."id"
join(Left, author::Relation::Post.def()):
SELECT FROM "author" LEFT JOIN "post" ON "author"."id" = "post"."author_id"
join_rev(Inner, post::Relation::Author.def()) from author:
SELECT FROM "author" INNER JOIN "post" ON "post"."author_id" = "author"."id"
第一行和第三行连的是同一条外键,一条从 post 出发、一条从 author 出发:有了 join_rev,不在 author 上声明反向关系也能从 author 连到 post。JoinType 由调用方给,RelationDef 里不存。
三个 trait:RelationTrait、Related、Linked
RelationTrait 是 Relation 枚举实现的 trait,只有一个必需方法:
pub trait RelationTrait: Iterable + Debug + 'static {
/// The [`RelationDef`] describing this relation.
fn def(&self) -> RelationDef;
// ...
}
枚举的每个分支是一条外键连线。一个实体对另一个实体可以有好几条(post 的作者和编辑都指向 author),所以连线按名字区分,不按目标实体区分。
Related<R> 反过来按目标实体区分,说「到 R 走哪条」:
pub trait Related<R>
where
R: EntityTrait,
{
/// The relation from `Self` to `R`.
fn to() -> RelationDef;
/// Junction table relation, if `Self <-> R` is many-to-many.
fn via() -> Option<RelationDef> {
None
}
/// Build a [`Select<R>`] pre-joined with `Self` via this relation.
fn find_related() -> Select<R> {
Select::<R>::new().join_join_rev(JoinType::InnerJoin, Self::to(), Self::via())
}
}
一对多、多对一只实现 to();多对多再实现 via(),返回「从自己到中间表」的那条线,to() 返回「从中间表到 R」的那条。find_related 从 R 出发,先 join_rev(to) 接上中间表,再 join_rev(via) 接上自己。post 到 tag 的实测:
Related<tag>::find_related():
SELECT FROM "tag" INNER JOIN "post_tag" ON "post_tag"."tag_id" = "tag"."id" INNER JOIN "post" ON "post"."id" = "post_tag"."post_id"
ModelTrait::find_related 在这个查询上再按 self 的主键加条件,那部分在 join 与 find_related。
Related<R> 一对实体只能实现一次,这是 Rust 的 trait 规则。post 到 author 有两条线时,Related<author> 只能指其中一条;要走另一条,或者要连续走好几跳,用 Linked:
pub trait Linked {
/// Entity at the start of the chain.
type FromEntity: EntityTrait;
/// Entity reached at the end of the chain.
type ToEntity: EntityTrait;
/// Ordered chain of hops from `FromEntity` to `ToEntity`.
fn link(&self) -> Vec<LinkDef>;
/// Build a [`Select<ToEntity>`] that follows the chain.
fn find_linked(&self) -> Select<Self::ToEntity> {
find_linked(self.link().into_iter().rev(), JoinType::InnerJoin)
}
}
LinkDef 就是 RelationDef 的类型别名。Linked 没有类型参数,由一个普通的 struct 来实现,所以同一对实体之间可以有任意多个 Linked,各起各的名字。
1.x 写法和 2.0 写法:同一个 RelationDef
1.x 的展开写法手写 Relation 枚举和 Related impl(本站就是这样写的):
#[derive(Copy, Clone, Debug, EnumIter, DeriveRelation)]
pub enum Relation {
#[sea_orm(
belongs_to = "super::author::Entity",
from = "Column::AuthorId",
to = "super::author::Column::Id",
on_delete = "Cascade"
)]
Author,
}
impl Related<super::author::Entity> for Entity {
fn to() -> RelationDef {
Relation::Author.def()
}
}
2.0 的 dense 写法把关系写成 Model 的字段:
#[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 author_id: i32,
#[sea_orm(belongs_to, from = "author_id", to = "id", on_delete = "Cascade")]
pub author: BelongsTo<super::author::Entity>,
#[sea_orm(has_many, via = "post_tag")]
pub tags: HasMany<super::tag::Entity>,
}
两者是不是同一种关系,不用比行为,看展开就够了。#[sea_orm::model](expand_sea_orm_model)把关系字段从 Model 里拿走,放进 ModelEx(见 DeriveEntityModel 与 dense 格式),同时为每个字段生成一个 Relation 分支,带着 1.x 的那套属性(expand_belongs_to_into 里是 #[sea_orm(#belongs_to = #related_entity, from = #from, to = #to, #extra)])。cargo expand --lib dense::post 的相关几行:
/// Generated by sea-orm-macros
pub enum Relation {
/// Generated by sea-orm-macros
#[sea_orm(
belongs_to = "super::author::Entity",
from = "Column::AuthorId",
to = "super::author::Column::Id",
on_delete = "Cascade"
)]
Author,
}
// ...
impl sea_orm::entity::RelationTrait for Relation {
fn def(&self) -> sea_orm::entity::RelationDef {
match self {
Self::Author => {
Entity::belongs_to(super::author::Entity)
.from(Column::AuthorId)
.to(super::author::Column::Id)
.on_delete(sea_orm::prelude::ForeignKeyAction::Cascade)
.into()
}
// ...
}
}
}
/// Generated by sea-orm-macros
impl Related<super::author::Entity> for Entity {
fn to() -> RelationDef {
Relation::Author.def()
}
}
/// Generated by sea-orm-macros
impl Related<super::tag::Entity> for Entity {
fn to() -> RelationDef {
super::post_tag::Relation::Tag.def()
}
fn via() -> Option<RelationDef> {
Some(<super::post_tag::Entity as Related<Entity>>::to().rev())
}
}
dense 的 Relation 枚举之后照样交给 DeriveRelation(impl_relation_trait),legacy::post 展开出来的 RelationTrait impl 与上面逐行相同。dense 写法自己没有一份「生成 RelationDef」的代码,它只是替你写了 1.x 的枚举和 Related impl。
用运行结果再核对一遍:src/bin/rel_defs.rs 对每一对关系同时比 == 和 Debug 字符串。
fn compare(name: &str, a: RelationDef, b: RelationDef) {
let (da, db) = (format!("{a:?}"), format!("{b:?}"));
println!("{name}: PartialEq={} Debug-equal={}", a == b, da == db);
println!(" {da}");
if da != db {
println!(" dense: {db}");
}
}
// ...
compare("post->author", legacy::post::Relation::Author.def(), dense::post::Relation::Author.def());
compare("author->post", legacy::author::Relation::Post.def(), dense::author::Relation::Post.def());
// ... Related<tag>::to / via, staff 的 ReportsTo 同理
let no_cascade = RelationDef { on_delete: None, ..legacy::post::Relation::Author.def() };
println!("post->author vs same without on_delete: PartialEq={}", no_cascade == legacy::post::Relation::Author.def());
post->author: PartialEq=true Debug-equal=true
RelationDef { rel_type: HasOne, from_tbl: Table(TableName(None, DynIden("post")), None), to_tbl: Table(TableName(None, DynIden("author")), None), from_col: Unary(DynIden("author_id")), to_col: Unary(DynIden("id")), is_owner: false, skip_fk: false, on_delete: Some(Cascade), on_update: None, on_condition: false, fk_name: None, condition_type: All }
author->post: PartialEq=true Debug-equal=true
RelationDef { rel_type: HasMany, from_tbl: Table(TableName(None, DynIden("author")), None), to_tbl: Table(TableName(None, DynIden("post")), None), from_col: Unary(DynIden("id")), to_col: Unary(DynIden("author_id")), is_owner: true, skip_fk: false, on_delete: None, on_update: None, on_condition: false, fk_name: None, condition_type: All }
Related<tag>::to: PartialEq=true Debug-equal=true
RelationDef { rel_type: HasOne, from_tbl: Table(TableName(None, DynIden("post_tag")), None), to_tbl: Table(TableName(None, DynIden("tag")), None), from_col: Unary(DynIden("tag_id")), to_col: Unary(DynIden("id")), is_owner: false, skip_fk: false, on_delete: None, on_update: None, on_condition: false, fk_name: None, condition_type: All }
Related<tag>::via: PartialEq=true Debug-equal=true
RelationDef { rel_type: HasOne, from_tbl: Table(TableName(None, DynIden("post")), None), to_tbl: Table(TableName(None, DynIden("post_tag")), None), from_col: Unary(DynIden("id")), to_col: Unary(DynIden("post_id")), is_owner: true, skip_fk: false, on_delete: None, on_update: None, on_condition: false, fk_name: None, condition_type: All }
staff reports_to: PartialEq=true Debug-equal=true
RelationDef { rel_type: HasOne, from_tbl: Table(TableName(None, DynIden("staff")), None), to_tbl: Table(TableName(None, DynIden("staff")), None), from_col: Unary(DynIden("reports_to_id")), to_col: Unary(DynIden("id")), is_owner: false, skip_fk: false, on_delete: None, on_update: None, on_condition: false, fk_name: None, condition_type: All }
post->author vs same without on_delete: PartialEq=true
五对全部相同。最后一行说明为什么还要比 Debug:PartialEq for RelationDef 只比 rel_type、两张表、两组列和 is_owner 六项,on_delete、fk_name、on_condition 不同也算相等。它回答的是「是不是同一条连线」,不是「两个定义是否完全一样」。
author->post 那行还有两点值得看。一是 on_delete: None:post 那边写了 Cascade,到 author 这边没了。has_many 的 EntityTrait::has_many 是 RelationBuilder::from_rel(RelationType::HasMany, R::to().rev(), true),即拿对方 Related<Self>::to() 的结果倒过来,from_rel 只抄表和列,外键属性全部重置。外键只在持有它的那一边声明一次,反向关系只是连线,不是第二个外键。二是因此 has_many / has_one 要求对方实现了 Related<Self>(R: EntityTrait + Related<Self>),对方没有这个 impl,这边就编译不过;对方有好几条线指过来时,用 has_many_via(R, rel) 点名(DeriveRelation 的 via_rel 属性生成的就是它)。
问: dense 的 post 声明了 tags: HasMany<tag::Entity>,可展开出来的 Relation 枚举里只有 Author,没有 Tag。是漏了吗?
答: 没漏。Relation 的每个分支是一条外键连线,post 到 tag 之间没有外键,只有经过 post_tag 的两条线,它们属于 post_tag 的 Relation。expand_has_many_into 只在 has_many && via.is_none() 时才加分支;带 via 时只生成 Related<tag> 的 to / via(上面展开的最后一段)。1.x 写法里多对多也是这样写的:Relation 不出现 Tag,Related<tag> 手写两条线。
自引用:同一张表出现两次
staff.reports_to_id 指向 staff.id。RelationDef 照常生成(上面输出里 from_tbl、to_tbl 都是 staff),但直接拿去 join 会出问题。src/bin/rel_db.rs 真的发给数据库:
let r = staff::Entity::find()
.join(JoinType::LeftJoin, staff::Relation::ReportsTo.def())
.all(&db)
.await;
println!("result: {r:?}");
sqlx 日志里实际发出的语句和结果:
INFO sqlx::query: summary="SELECT \"staff\".\"id\", \"staff\".\"name\", \"staff\".\"reports_to_id\" …" db.statement="\n\nSELECT \"staff\".\"id\", \"staff\".\"name\", \"staff\".\"reports_to_id\" FROM \"staff\" LEFT JOIN \"staff\" ON \"staff\".\"reports_to_id\" = \"staff\".\"id\"\n" rows_affected=0 rows_returned=0 elapsed=904.042µs elapsed_secs=0.000904042
result: Err(Query(SqlxError(Database(PgDatabaseError { severity: Error, code: "42712", message: "table name \"staff\" specified more than once", detail: None, hint: None, position: None, where: None, schema: None, table: None, column: None, data_type: None, constraint: None, file: Some("parse_relation.c"), line: Some(463), routine: Some("checkNameSpaceConflicts") }))))
From<RelationDef> for Condition 用的是表名,两张表都叫 staff,ON "staff"."reports_to_id" = "staff"."id" 本身就分不清哪个是哪个,Postgres 在解析阶段就拒绝了。sea-orm 在构造时不检查这一点,错误要到发出去才出现。
解决办法是给其中一张表起别名,RelationDef 里 TableRef 可以带别名,ON 条件会改用别名。手动的办法是 join_as,它把 to_tbl 换成带别名的版本:
staff join_as(Left, ReportsTo, "mgr"):
SELECT FROM "staff" LEFT JOIN "staff" AS "mgr" ON "staff"."reports_to_id" = "mgr"."id"
另一个办法是 Linked,它每一跳都自动起别名。实验里的 legacy::staff 不实现 Related<Entity> for Entity,而是写一个 Linked:
pub struct ManagerLink;
impl Linked for ManagerLink {
type FromEntity = Entity;
type ToEntity = Entity;
fn link(&self) -> Vec<RelationDef> {
vec![Relation::ReportsTo.def()]
}
}
find_linked 把这串连线倒着走,从 ToEntity 出发,第 i 跳的那张表一律叫 r{i}:
pub(crate) fn find_linked<I, E>(links: I, join: JoinType) -> Select<E>
where
I: Iterator<Item = LinkDef>,
E: EntityTrait,
{
let mut select = Select::new();
for (i, mut rel) in links.enumerate() {
let from_tbl = format!("r{i}").into_iden();
let to_tbl = if i > 0 {
format!("r{}", i - 1).into_iden()
} else {
rel.to_tbl.sea_orm_table().clone()
};
let table_ref = rel.from_tbl;
let mut condition = match rel.condition_type {
ConditionType::All => Condition::all(),
ConditionType::Any => Condition::any(),
};
condition = condition.add(join_tbl_on_condition(
from_tbl.clone(),
to_tbl.clone(),
rel.from_col,
rel.to_col,
));
if let Some(f) = rel.on_condition.take() {
condition = condition.add(f(from_tbl.clone(), to_tbl.clone()));
}
select.query().join_as(join, table_ref, from_tbl, condition);
}
select
}
FROM 后面是不带别名的 ToEntity,链上其余的表都是 r0、r1……,所以即使整条链都在同一张表上也不会撞名。ModelTrait::find_linked 再按最后一个别名 r{len-1} 加上起点的主键条件:
staff ManagerLink.find_linked():
SELECT FROM "staff" INNER JOIN "staff" AS "r0" ON "r0"."reports_to_id" = "staff"."id"
model.find_linked(ManagerLink):
SELECT "staff"."id", "staff"."name", "staff"."reports_to_id" FROM "staff" INNER JOIN "staff" AS "r0" ON "r0"."reports_to_id" = "staff"."id" WHERE "r0"."id" = 7
读法:r0 是起点(id = 7 的员工),FROM "staff" 是要查的上级,条件是「r0 的 reports_to_id 等于这一行的 id」。发给数据库的版本(rel_db.rs 的第二部分,查 alice 的上级):
INFO sqlx::query: summary="SELECT \"staff\".\"id\", \"staff\".\"name\", \"staff\".\"reports_to_id\" …" db.statement="\n\nSELECT \"staff\".\"id\", \"staff\".\"name\", \"staff\".\"reports_to_id\" FROM \"staff\" INNER JOIN \"staff\" AS \"r0\" ON \"r0\".\"reports_to_id\" = \"staff\".\"id\" WHERE \"r0\".\"id\" = $1 LIMIT $2\n" rows_affected=1 rows_returned=1 elapsed=1.487542ms elapsed_secs=0.001487542
result: Some(Model { id: 1, name: "boss", reports_to_id: None })
Linked 的每一跳直到 2.0.4 都忽略 condition_type上面 find_linked 里 match rel.condition_type 那四行是 #3203 加的,首次发布于 2.0.4。之前这里写死 Condition::all()。PR 说明的原话:From<RelationDef> for Condition 会照 ConditionType::Any 从 Condition::any() 开始,「but LinkDef is the same RelationDef, and both linked paths build each hop starting from Condition::all() unconditionally」,于是声明了 Any 的一跳「gets its on_condition ANDed instead of ORed, silently returning different rows」。修法是两条路径(find_linked 和 left_join_linked)都改成按 rel.condition_type 选根条件,「Links using the default ConditionType::All produce exactly the same SQL as before」。2.0.4 的 release notes 把它记为「Respect condition_type in linked joins」。
dense 写法的自引用用 self_ref 属性,宏生成的仍然是普通的 Relation 分支,而且不生成 Related<Entity> for Entity。实验里的 dense::staff:
#[sea_orm(
self_ref,
relation_enum = "ReportsTo",
relation_reverse = "Manages",
from = "reports_to_id",
to = "id"
)]
pub reports_to: BelongsTo<Option<Entity>>,
#[sea_orm(self_ref, relation_enum = "Manages", relation_reverse = "ReportsTo")]
pub manages: HasMany<Entity>,
展开后:
pub enum Relation {
/// Generated by sea-orm-macros
#[sea_orm(
belongs_to = "Entity",
from = "Column::ReportsToId",
to = "Column::Id",
)]
ReportsTo,
/// Generated by sea-orm-macros
#[sea_orm(has_many = "Entity", via_rel = "Relation::ReportsTo")]
Manages,
}
// ...
Self::ReportsTo => {
Entity::belongs_to(Entity)
.from(Column::ReportsToId)
.to(Column::Id)
.into()
}
Self::Manages => Entity::has_many_via(Entity, Relation::ReportsTo).into(),
relation_enum 是必需的:同一个实体既是起点又是终点,分支名不能从目标实体名推出来。Manages 就是 ReportsTo 倒过来(from_col: id、to_col: reports_to_id、HasMany、is_owner: true)。整个展开里没有 impl Related<Entity> for Entity。自引用怎么加载见 Loader 与 N+1;自己写 join 时,前面的撞名规则照样适用。多对多的自引用(关注者表)走 RelatedSelfVia,它和 Related 的区别只是 via 不可选。
staff::Entity::find().join(JoinType::LeftJoin, staff::Relation::ReportsTo.def()) 会撞名。把 LeftJoin 换成 InnerJoin、把 join 换成 join_rev,四种组合里有几种能被 Postgres 执行?平台:M2 Max、macOS、rustc 1.99.0、Postgres 16.14。答案在页末。BelongsTo:把基数写进类型
dense 写法里 belongs_to 字段的类型有两种:BelongsTo<E> 和 BelongsTo<Option<E>>。宏在 CompoundType::from_type 里从字段类型认出是哪一种(CardinalityKind::Required / Optional)。这个区分不进 RelationDef:CardinalityKind 只在 model_ex.rs 和 active_model_ex.rs 里用到,expand_belongs_to_into 生成的 Relation 分支两种写法一模一样。上面的 staff.reports_to 是 BelongsTo<Option<Entity>>,post.author 是 BelongsTo<author::Entity>,它们的 Relation 分支和 RelationDef 结构完全一致。所以它也不决定 join 用 INNER 还是 LEFT。
它影响的是两处类型。读出来的 BelongsTo<T> 是 Unloaded | Loaded(T::Loaded),而 Loaded 由 BelongsToCardinality 决定:
| 字段类型 | Loaded(..) 里装的 | 能不能表达「没有」 |
|---|---|---|
BelongsTo<E> | Box<E::ModelEx> | 不能 |
BelongsTo<Option<E>> | Option<Box<E::ModelEx>> | Loaded(None):加载过,外键是 NULL |
HasOne<E>(旧写法,仍然支持) | Option<Box<E::ModelEx>> | Loaded(None) |
写入那一侧,ActiveModelEx 的方法不同。cargo expand 里 post 只有 set_author,staff 多两个:
pub fn set_reports_to_option(
mut self,
v: Option<impl Into<ActiveModelEx>>,
) -> Self {
if v.is_none() {
self.reports_to_id = sea_orm::Set(None);
}
self.reports_to = sea_orm::ActiveBelongsTo::<Option<Entity>>::set(v);
self
}
// ...
pub fn clear_reports_to(mut self) -> Self {
self.reports_to_id = sea_orm::Set(None);
self.reports_to = sea_orm::ActiveBelongsTo::Set(None);
self
}
「解除关联」就是把外键列设成 NULL。外键列不可空时没有这个操作,这些方法也就不生成。嵌套写入的完整过程见 ActiveModelEx。
基数和外键列是否可空必须一致,宏在 expand_active_belongs_to_into 里检查。外键 author_id: Option<i32> 却写 BelongsTo<author::Entity>:
error: BelongsTo<Entity> requires non-Option `from` fields
--> src/bin/bt_mismatch.rs:12:34
|
12 | #[sea_orm(belongs_to, from = "author_id", to = "id")]
| ^^^^^^^^^^^
反过来,author_id: i32 配 BelongsTo<Option<author::Entity>>:
error: BelongsTo<Option<Entity>> requires at least one `Option<T>` `from` field
--> src/bin/bt_mismatch2.rs:12:34
|
12 | #[sea_orm(belongs_to, from = "author_id", to = "id")]
| ^^^^^^^^^^^
「at least one」是给复合外键留的:几列里只要有一列可空,就算可选关联,解除时只把可空的那几列设成 NULL。
BelongsTo 是 2.0 正式版前最后一个 rc 才加的2.0.0-rc.43 之前,dense 写法里 belongs_to 字段只能写成 HasOne<E>,而 HasOne 有表示「没找到」的状态,「外键不可空所以一定有」这件事类型里表达不了。#3118(标题「Support non-null HasOne」)的说明写着「Adds support for non-nullable HasOne relations」,并「Remove the NotFound representation for empty HasOne relations, as this state should never be possible for non-nullable relations」。接着 #3133(「Introduce BelongsTo」)把它做成可选的:「belongs_to now accepts the recommended BelongsTo<E> / BelongsTo<Option<E>> ... or the legacy HasOne<E> field type」。两者都首次发布于 2.0.0-rc.43,release notes 的说法是「Cardinality lives in the type parameter, so the foreign-key nullability is expressed — and checked — at compile time」,上面两条编译错误就是这个检查。
动脑筋答案
src/bin/self_join_quiz.rs(staff 表由 rel_db 建好):
for jt in [JoinType::LeftJoin, JoinType::InnerJoin] {
let a = staff::Entity::find().join(jt, staff::Relation::ReportsTo.def()).all(&db).await;
println!("join {jt:?}: {}", a.map(|v| format!("Ok({} rows)", v.len())).unwrap_or_else(|e| e.to_string()));
let b = staff::Entity::find().join_rev(jt, staff::Relation::ReportsTo.def()).all(&db).await;
println!("join_rev {jt:?}: {}", b.map(|v| format!("Ok({} rows)", v.len())).unwrap_or_else(|e| e.to_string()));
}
join LeftJoin: Query Error: error returned from database: table name "staff" specified more than once at line 463
join_rev LeftJoin: Query Error: error returned from database: table name "staff" specified more than once at line 463
join InnerJoin: Query Error: error returned from database: table name "staff" specified more than once at line 463
join_rev InnerJoin: Query Error: error returned from database: table name "staff" specified more than once at line 463
0 种。join 接的是 rel.to_tbl,join_rev 接的是 rel.from_tbl,对 ReportsTo 来说两个都是没有别名的 staff;JoinType 只改 JOIN 前面的关键字,不碰表名。四条语句都是 FROM "staff" ... JOIN "staff" ON ...,Postgres 在解析阶段就拒绝。行尾的「at line 463」是错误响应里 line 字段(Postgres 源码 parse_relation.c 的行号),不是 SQL 的行号。
小结
RelationDef是关系的全部运行时信息:两端的表和列、HasOne/HasMany、is_owner、附加ON条件、外键 DDL 属性。belongs_to是HasOne加is_owner = false,没有单独的类型。From<RelationDef> for Condition生成ON from.col = to.col [AND/OR on_condition];join接to_tbl,join_rev接from_tbl,ON条件相同。RelationTrait按连线命名,Related<R>按目标实体选一条(多对多再给via),Linked是任意命名的连线序列,每跳自动起别名r0、r1……- dense 写法的
#[sea_orm::model]只是替你写出 1.x 的Relation枚举和Relatedimpl,再走同一个DeriveRelation,展开和运行结果都证明两种写法的RelationDef逐字段相同。PartialEq只比六个字段,不比on_delete。 - 反向的
has_many/has_one由对方的Related<Self>::to().rev()得来,只抄表和列,外键属性只在持有外键的一边声明。 - 自引用直接
join会得到42712 table name "staff" specified more than once;用join_as或Linked起别名。 BelongsTo<E>/BelongsTo<Option<E>>不改变RelationDef,只改变ModelEx里装的类型和ActiveModelEx能不能解除关联,并在编译期核对外键列的可空性。