join 与 find_related
主线位置:查询主线上「一次查两张表」的分支。关系 讲了
RelationDef怎么变成JOIN ... ON;这一页讲 sea-orm 拿这条 join 做什么:find_also_related把每一行拆成两个Model,find_with_related再把同一个父记录的行合并成一个Vec,find_related和has_related把关联表当成过滤条件。它们最后都走 Select 与过滤 里的Selector::all/one,每次只发一条语句。
flowchart TB sel["Select E"] -->|left_join| j["LEFT JOIN 语句"] j -->|"select_also: A_ 前缀"| two["SelectTwo"] j -->|"select_with: A_ 前缀 + ORDER BY 主键"| many["SelectTwoMany"] two -->|"B_ 前缀列"| m2["SelectTwoModel"] many -->|"B_ 前缀列"| m2 m2 -->|"每行一对"| pairs["Vec 父 Option 子"] pairs -->|consolidate| grouped["Vec 父 Vec 子"] sel -->|has_related| ex["WHERE EXISTS"] model["Model"] -->|find_related| inner["INNER JOIN + 主键过滤"]
实验接着用 Select 与过滤 的 author / post 两张表(ann 有 a1、a2、a3 三篇,bob 没有,cid 有 c1、c2,dee 有 d1),再加一个多对多:
CREATE TABLE tag (id serial PRIMARY KEY, name text NOT NULL);
CREATE TABLE post_tag (post_id int REFERENCES post(id), tag_id int REFERENCES tag(id),
PRIMARY KEY (post_id, tag_id));
INSERT INTO tag (name) VALUES ('rust'), ('sql');
INSERT INTO post_tag VALUES (1, 1), (2, 2), (4, 1), (4, 2);
实体是 关系 那一页的 dense 写法:author 有 #[sea_orm(has_many)] posts,post 有 belongs_to 的 author 和 #[sea_orm(has_many, via = "post_tag")] tags。平台 M2 Max、macOS、rustc 1.99.0、Postgres 16.14;SQL 取自 sqlx 的语句日志(实际发出的文本),需要看参数值的地方另开 debug-print。
几个 join 入口
Select 上有四个按实体名 join 的方法,left_join 是代表:
pub fn left_join<R>(self, _: R) -> Self
where
R: EntityTrait,
E: Related<R>,
{
self.join_join(JoinType::LeftJoin, E::to(), E::via())
}
参数 _: R 只用来推断类型,值本身不用。E: Related<R> 决定走哪条连线:to() 是到 R 的那条,多对多时 via() 是到中间表的那条,join_join 先 join via 再 join to。
| 方法 | join 类型 | 连线 |
|---|---|---|
left_join(R) / right_join(R) / inner_join(R) | LEFT / RIGHT / INNER | E::via()(若有)再 E::to(),用 join |
reverse_join(R) | INNER | R::to(),用 join_rev;只要求 R: Related<E> |
join(JoinType, RelationDef) / join_rev / join_as | 调用方给 | 调用方给 |
join / join_rev 各接哪张表、ON 条件怎么从 RelationDef 来、自引用为什么要别名,都在 关系。这些方法只加 JOIN,SELECT 列表还是 E 的列,结果仍然读成 E::Model;要把对面的列也取回来,就是下面的 find_also_related。
find_also_related:一行一对
find_also_related 是两步:
pub fn find_also_related<R>(self, r: R) -> SelectTwo<E, R>
where
R: EntityTrait,
E: Related<R>,
{
self.left_join(r).select_also(r)
}
两张表都有 id,同一行里两个 id 列分不开,所以 select_also 先给已有的列加前缀,再由 SelectTwo::new 把 R 的列加进来:
pub fn select_also<F>(mut self, _: F) -> SelectTwo<E, F>
where
F: EntityTrait,
{
self = self.apply_alias(SelectA.as_str());
SelectTwo::new(self.into_query())
}
前缀是 select_def! 定义的六个常量 SelectA 到 SelectF,值是 "A_" 到 "F_",两表用前两个,SelectThree 到 SelectSix 依次用到 F_。apply_alias 遍历 SELECT 列表:
pub(crate) fn apply_alias(mut self, pre: &str) -> Self {
self.query().exprs_mut_for_each(|sel| {
match &sel.alias {
Some(alias) => {
let alias = format!("{}{}", pre, alias.to_string().as_str());
sel.alias = Some(alias.into_iden());
}
None => {
let col = match &sel.expr {
SimpleExpr::Column(col_ref) => match col_ref.column() {
Some(col) => col,
None => {
panic!("cannot apply alias for Column with asterisk");
}
},
// ... AsEnum 分支同理
_ => panic!("cannot apply alias for expr other than Column or AsEnum"),
};
let alias = format!("{}{}", pre, col.to_string().as_str());
sel.alias = Some(alias.into_iden());
}
};
});
self
}
已有别名的列,别名前面加 A_;没有别名的,从表达式里取列名,加 A_ 当别名。R 那一侧由 prepare_select_col 直接生成 B_列名 的别名:
pub(crate) fn prepare_select_col<F, S, A>(selector: &mut S, alias: A)
where
F: EntityTrait,
S: QueryTrait<QueryStatement = SelectStatement>,
A: IdenStatic,
{
for col in <F::Column as Iterable>::iter() {
let alias = format!("{}{}", alias.as_str(), col.as_str());
selector.query().expr(SelectExpr {
expr: col.select_as(col.into_expr()),
alias: Some(alias.into_iden()),
window: None,
});
}
}
apply_alias 的 panic 分支直到 2.0.3 还会被走到apply_alias 只认得「列」和「枚举 CAST」两种没有别名的表达式。如果某列的 select_as 返回别的东西(比如为 citext 列生成的 CAST(.. AS text)),find_also_related 在发 SQL 之前就 panic。#3174(「find_also_related panics on entities generated by sea-orm-cli for citext columns」)报了这个。#3194 的说明写道:「Combined selects prefix the first-entity projection aliases, but can only infer an unaliased projection name from a plain column or an enum cast」,修法是「Preserve the column logical name as the projection alias whenever select_as produces a transformed expression」。这就是 into_select_expr 里那个 _ => Some(self.as_str().into_iden()) 分支:Select::new 现在就给这类列带上别名,apply_alias 走 Some(alias) 那条路。首次发布于 2.0.3,release notes 的兼容性说明提醒它「also changes the generated SQL for regular queries」:CAST("hello"."two" AS integer) 变成了 CAST("hello"."two" AS integer) AS "two"。
src/bin/related.rs 的第一段:
let rows = author::Entity::find()
.find_also_related(post::Entity)
.order_by_asc(author::Column::Id)
.order_by_asc(post::Column::Id)
.all(&db)
.await?;
for (a, p) in &rows {
println!("{} {:?}", a.name, p.as_ref().map(|p| &p.title));
}
INFO sqlx::query: summary="SELECT \"author\".\"id\" AS \"A_id\", …" db.statement="\n\nSELECT \"author\".\"id\" AS \"A_id\", \"author\".\"name\" AS \"A_name\", \"post\".\"id\" AS \"B_id\", \"post\".\"title\" AS \"B_title\", \"post\".\"likes\" AS \"B_likes\", \"post\".\"author_id\" AS \"B_author_id\" FROM \"author\" LEFT JOIN \"post\" ON \"author\".\"id\" = \"post\".\"author_id\" ORDER BY \"author\".\"id\" ASC, \"post\".\"id\" ASC\n" rows_affected=7 rows_returned=7 elapsed=3.01025ms elapsed_secs=0.00301025
ann Some("a1")
ann Some("a2")
ann Some("a3")
bob None
cid Some("c1")
cid Some("c2")
dee Some("d1")
一条语句、七行,每行一对:ann 出现三次,没有文章的 bob 是 None。find_also_related 自己不加 ORDER BY,上面的排序是手写的。
拆回两个 Model
SelectTwo 的 all 用 SelectTwoModel 读每一行,它的 SelectorTrait 实现:
impl<M, N> SelectorTrait for SelectTwoModel<M, N>
where
M: FromQueryResult + Sized,
N: FromQueryResult + Sized,
{
type Item = (M, Option<N>);
fn from_raw_query_result(res: QueryResult) -> Result<Self::Item, DbErr> {
Ok((
M::from_query_result(&res, SelectA.as_str())?,
N::from_query_result_optional(&res, SelectB.as_str())?,
))
}
}
第二个参数就是列名前缀:读 M 的 id 字段去找 A_id 列,读 N 的去找 B_id。左边用 from_query_result,出错就是错;右边用 from_query_result_optional,LEFT JOIN 没匹配上时那几列全是 NULL,要变成 None。问题出在它的默认实现(FromQueryResult::from_query_result_optional):
fn from_query_result_optional(res: &QueryResult, pre: &str) -> Result<Option<Self>, DbErr> {
Ok(Self::from_query_result(res, pre).ok())
// would really like to do the following, but can't without version bump:
// match Self::from_query_result_nullable(res, pre) {
// Ok(v) => Ok(Some(v)),
// Err(TryGetError::Null(_)) => Ok(None),
// Err(TryGetError::DbErr(err)) => Err(err),
// }
}
.ok() 把所有错误都变成 None,不只是「全是 NULL」。DeriveEntityModel 和 derive(FromQueryResult) 都不覆盖这个方法(宏只生成 from_query_result 和 from_query_result_nullable),注释里那段「需要升大版本才能改」的代码,到 2.0 也没有换上;1.1.20 里是同样的写法。src/bin/also_swallow.rs 用一个读不出来的结构体做右边:
// `likes` is int4 in the table; String cannot decode it.
#[derive(Debug, FromQueryResult)]
#[allow(dead_code)]
struct BadPost {
id: i32,
likes: String,
}
// ...
let rows = author::Entity::find()
.find_also_related(post::Entity)
.order_by_asc(author::Column::Id)
.into_model::<author::Model, BadPost>()
.all(&db)
.await;
// 打印行数和前三行
// ...
let rows = post::Entity::find()
.find_also_related(author::Entity)
.into_model::<BadPost, author::Model>()
.all(&db)
.await;
println!("{:?}", rows.map(|v| v.len()));
--- B side cannot decode
Ok, 7 rows
ann None
ann None
ann None
--- same struct, A side
Err(Query(SqlxError(ColumnDecode { index: "\"A_likes\"", source: "mismatched types; Rust type `core::option::Option<alloc::string::String>` (as SQL type `TEXT`) is not compatible with SQL type `INT4`" })))
同一个类型错误,放在左边是 ColumnDecode,放在右边是一列安静的 None,看起来和「没有关联记录」一模一样。实体字段类型和列类型对不上(改了列类型没改实体)时,find_also_related / find_with_related 的右边就会这样「全都没有关联」。
find_both_related(2.0.0-rc.38,#2997)是 INNER JOIN 版本,返回 SelectTwoRequired,右边用 from_query_result,类型是 (E::Model, F::Model),没有 Option,也就没有上面的问题。同样的数据,它返回 6 行(bob 没了):
INFO sqlx::query: summary="SELECT \"author\".\"id\" AS \"A_id\", …" db.statement="\n\nSELECT \"author\".\"id\" AS \"A_id\", \"author\".\"name\" AS \"A_name\", \"post\".\"id\" AS \"B_id\", \"post\".\"title\" AS \"B_title\", \"post\".\"likes\" AS \"B_likes\", \"post\".\"author_id\" AS \"B_author_id\" FROM \"author\" INNER JOIN \"post\" ON \"author\".\"id\" = \"post\".\"author_id\"\n" rows_affected=6 rows_returned=6 elapsed=583.208µs elapsed_secs=0.000583208
6 rows
find_with_related:一条 join,再在内存里合并
find_with_related 和 find_also_related 只差最后一步:
pub fn find_with_related<R>(self, r: R) -> SelectTwoMany<E, R>
where
R: EntityTrait,
E: Related<R>,
{
self.left_join(r).select_with(r)
}
select_with 同样 apply_alias("A_"),构造的是 SelectTwoMany,它的 new 比 SelectTwo 多一步(prepare_order_by):
pub(crate) fn new(query: SelectStatement) -> Self {
Self::new_without_prepare(query)
.prepare_select()
.prepare_order_by()
}
// ...
fn prepare_order_by(mut self) -> Self {
for col in <E::PrimaryKey as Iterable>::iter() {
self.query.order_by((E::default(), col), Order::Asc);
}
self
}
执行时 SelectTwoMany::all 先按 SelectTwoModel 读出 Vec<(E::Model, Option<F::Model>)>,再交给 consolidate_query_result。它按父表主键的列数(编译期常量 ARITY)选一种键:单列主键的键是一个 Value,两列是 (Value, Value),更多是 Vec<Value>,然后进 consolidate_query_result_of:
fn consolidate_query_result_of<L, R, KEY: ModelKey<L>>(
mut rows: Vec<(L::Model, Option<R::Model>)>,
model_key: KEY,
) -> Vec<(L::Model, Vec<R::Model>)>
where
L: EntityTrait,
R: EntityTrait,
{
// group items by unique key on left model
let mut hashmap: HashMap<KEY::Type, Vec<R::Model>> =
rows.iter_mut().fold(HashMap::new(), |mut acc, row| {
let key = model_key.get(&row.0); // keep left model in place
if let Some(value) = row.1.take() {
// take ownership of right model
if let Some(vec) = acc.get_mut(&key) {
vec.push(value)
} else {
acc.insert(key, vec![value]);
}
} else {
acc.entry(key).or_default(); // insert empty vec
}
acc
});
// re-iterate so that we keep the same order
rows.into_iter()
.filter_map(|(l_model, _)| {
// right model is empty here already
let l_pk = model_key.get(&l_model);
// the first time we encounter a left model, the value is taken
// subsequently the key will be empty
let r_models = hashmap.remove(&l_pk);
r_models.map(|r_models| (l_model, r_models))
})
.collect()
}
两遍。第一遍按父记录的主键值分组,把右边的 Model 从行里拿走(take)放进对应的 Vec;右边是 None(LEFT JOIN 没匹配上)时也登记这个键,值是空 Vec。第二遍按原来的行序走,父记录第一次出现时从表里 remove 出它的 Vec,以后再出现,键已经不在了,filter_map 把这一行丢掉。所以:
- 合并的依据只是父表主键的值,不比较父记录的其他列。
- 不要求同一个父记录的行相邻,
ORDER BY对正确性不是必需的;父记录的顺序是它在结果里第一次出现的顺序,子记录的顺序是行序。 - 右边的
None不会变成Vec里的元素,没有子记录就是空Vec。
既然不依赖相邻,构造时那个 ORDER BY 父表主键是做什么的?它比这个算法早。2021 年的第一版 SelectTwoMany 就有 prepare_order_by,那时的合并算法只看「这一行和上一行是不是同一个父记录」,必须靠排序把同一个父记录的行排在一起。那个算法还留在文件里,叫 consolidate_query_result_of_ordered_rows,注释是「This is the legacy consolidate algorithm. Kept for reference」,标了 #[allow(dead_code)]。现在 ORDER BY 的作用只剩让父记录的顺序确定。
#1728(「Adding find_with_linked」,首次出现在 0.12.0-rc.4,正式版 0.12.1)把上面那个按相邻行合并的算法换成了按主键值分组的 HashMap。第一遍只在右边有值时才往表里插键,没有子记录的父记录在表里没有键,第二遍 hashmap.remove 拿到 None,被 filter_map 丢掉。#1800 的标题直接说了现象:「find_with_related does not return records with no related entity. It just records that have more than one related entities.」,期望是「return A with an empty list of Bs」。修复是一个 4 行的提交(「Fix find_with_related #1800」),给 if let Some(value) 加上 else,在键不存在时插入空 Vec;首次发布于 0.12.2,CHANGELOG 记为「Fixed find_with_related consolidation logic」。今天的 acc.entry(key).or_default(); // insert empty vec 就是这个 else 的后来写法。
related.rs 的第二、三段,一对多和经过 post_tag 的多对多:
let rows = author::Entity::find().find_with_related(post::Entity).all(&db).await?;
for (a, ps) in &rows {
println!("{} {:?}", a.name, ps.iter().map(|p| &p.title).collect::<Vec<_>>());
}
// ...
let rows = post::Entity::find()
.filter(post::Column::AuthorId.eq(1))
.find_with_related(tag::Entity)
.all(&db)
.await?;
--- 2. author find_with_related post
INFO sqlx::query: summary="SELECT \"author\".\"id\" AS \"A_id\", …" db.statement="\n\nSELECT \"author\".\"id\" AS \"A_id\", \"author\".\"name\" AS \"A_name\", \"post\".\"id\" AS \"B_id\", \"post\".\"title\" AS \"B_title\", \"post\".\"likes\" AS \"B_likes\", \"post\".\"author_id\" AS \"B_author_id\" FROM \"author\" LEFT JOIN \"post\" ON \"author\".\"id\" = \"post\".\"author_id\" ORDER BY \"author\".\"id\" ASC\n" rows_affected=7 rows_returned=7 elapsed=637.5µs elapsed_secs=0.0006375
ann ["a2", "a3", "a1"]
bob []
cid ["c1", "c2"]
dee ["d1"]
--- 3. post find_with_related tag (via post_tag)
INFO sqlx::query: summary="SELECT \"post\".\"id\" AS \"A_id\", …" db.statement="\n\nSELECT \"post\".\"id\" AS \"A_id\", \"post\".\"title\" AS \"A_title\", \"post\".\"likes\" AS \"A_likes\", \"post\".\"author_id\" AS \"A_author_id\", \"tag\".\"id\" AS \"B_id\", \"tag\".\"name\" AS \"B_name\" FROM \"post\" LEFT JOIN \"post_tag\" ON \"post\".\"id\" = \"post_tag\".\"post_id\" LEFT JOIN \"tag\" ON \"post_tag\".\"tag_id\" = \"tag\".\"id\" WHERE \"post\".\"author_id\" = $1 ORDER BY \"post\".\"id\" ASC\n" rows_affected=3 rows_returned=3 elapsed=2.0515ms elapsed_secs=0.0020515
a1 ["rust"]
a2 ["sql"]
a3 []
两处值得看。ann 的三篇是 a2, a3, a1:ORDER BY 只有父表主键,同一个父记录内部的行序由 Postgres 决定,这一次恰好不是插入顺序,要稳定的子记录顺序得自己再加一个 order_by。多对多的版本是两个 LEFT JOIN(先中间表、再 tag),中间表的列不进 SELECT。
自己加的 order_by 只能排在内置的那个后面,所以按别的列给父记录排序是做不到的(with_limit.rs 中间一段):
let rows = author::Entity::find()
.find_with_related(post::Entity)
.order_by_desc(author::Column::Name)
.all(&db)
.await?;
DEBUG sea_orm::driver::sqlx_postgres: SELECT "author"."id" AS "A_id", "author"."name" AS "A_name", "post"."id" AS "B_id", "post"."title" AS "B_title", "post"."likes" AS "B_likes", "post"."author_id" AS "B_author_id" FROM "author" LEFT JOIN "post" ON "author"."id" = "post"."author_id" ORDER BY "author"."id" ASC, "author"."name" DESC
...
ann ["a2", "a3", "a1"]
bob []
cid ["c1", "c2"]
dee ["d1"]
name DESC 成了第二排序键,主键唯一,它永远不起作用。要换顺序,得像 Select 与过滤 里说的那样先 QueryTrait::query(&mut q).clear_order_by()。合并算法不在乎行序,清掉以后结果依旧正确,只是顺序变了。
ORDER BY,改成按 post.likes 倒序,下面这段返回的第一个作者是谁?cid 的两篇是什么顺序? let mut q = author::Entity::find().find_with_related(post::Entity);
QueryTrait::query(&mut q).clear_order_by();
let rows = q.order_by_desc(post::Column::Likes).all(&db).await?;
limit 限制的是 join 后的行
SelectTwoMany 也实现了 QuerySelect,所以 .limit(n) 能编译,它改的是那条 join 语句的 LIMIT。src/bin/with_limit.rs:
for n in [3, 4, 5] {
println!("--- find_with_related(post).limit({n})");
let rows = author::Entity::find().find_with_related(post::Entity).limit(n).all(&db).await?;
println!(" {} authors", rows.len());
show(&rows);
}
--- find_with_related(post).limit(3)
DEBUG sea_orm::driver::sqlx_postgres: SELECT "author"."id" AS "A_id", "author"."name" AS "A_name", "post"."id" AS "B_id", "post"."title" AS "B_title", "post"."likes" AS "B_likes", "post"."author_id" AS "B_author_id" FROM "author" LEFT JOIN "post" ON "author"."id" = "post"."author_id" ORDER BY "author"."id" ASC LIMIT 3
INFO sqlx::query: summary="SELECT \"author\".\"id\" AS \"A_id\", …" db.statement="\n\nSELECT \"author\".\"id\" AS \"A_id\", \"author\".\"name\" AS \"A_name\", \"post\".\"id\" AS \"B_id\", \"post\".\"title\" AS \"B_title\", \"post\".\"likes\" AS \"B_likes\", \"post\".\"author_id\" AS \"B_author_id\" FROM \"author\" LEFT JOIN \"post\" ON \"author\".\"id\" = \"post\".\"author_id\" ORDER BY \"author\".\"id\" ASC LIMIT $1\n" rows_affected=3 rows_returned=3 elapsed=3.539541ms elapsed_secs=0.003539541
1 authors
ann ["a1", "a2", "a3"]
--- find_with_related(post).limit(4)
...
2 authors
ann ["a1", "a2", "a3"]
bob []
--- find_with_related(post).limit(5)
...
3 authors
ann ["a1", "a2", "a3"]
bob []
cid ["c1"]
LIMIT 作用在 join 之后的行上,sea-orm 不改写它。limit(3) 的三行全是 ann 的,于是只有 1 个作者;limit(5) 有 3 个作者,但 cid 只拿到了 c1,c2 落在第 6 行被截掉了,返回值里看不出它不完整。offset 一样按行算,翻页会把一个作者的文章切到两页上。
想要「前 n 个作者,各带全部文章」,得分两步:先对父表 limit(或 分页),再为这批父记录查子表,这正是 Loader 与 N+1 做的事。源码里 SelectTwoMany::all 后面留着一段注释,说的是同一件事:
// pub fn paginate()
// we could not implement paginate easily, if the number of children for a
// parent is larger than one page, then we will end up splitting it in two pages
// so the correct way is actually perform query in two stages
// paginate the parent model and then populate the children
SelectTwoMany 上因此只有 all,没有 paginate,也没有 one:
error[E0599]: no method named `one` found for struct `SelectTwoMany<E, F>` in the current scope
--> src/bin/two_many_one.rs:7:66
|
7 | let r = author::Entity::find().find_with_related(post::Entity).one(&db).await?;
| ---------------------- ^^^
| |
| method `one` is available on `sea_orm::Select<exp_seaorm_select1::author::Entity>`
SelectTwoMany::one 在 0.9.0 被删掉了早期的 SelectTwoMany 有 one(),走的是通用的 Selector::one,先 limit(1) 再读一行,返回 Option<(Model, Option<Model>)>:一对多查询只拿回一个子记录。#486(「Allow to fetch entity and all related ones」)说「For a One-to-Many relation this is really weird, what is expected is Option<(Model, Vec<Model>)>」。#813(「Drop SelectTwoMany::one() method」)的处理不是修正返回类型,而是删掉:理由是它「returns Option<(E, Option<F>)> and the return value is a pair of models instead of (E, Vec<F>) which is weird select result for a one-to-many relationship」。首次发布于 0.9.0,CHANGELOG 给的替代办法是「first querying a single model from Entity, then use ModelTrait::find_related on the model」,SelectTwoMany::all 的文档注释里至今留着这段话。正确的 one 本来就做不出来:LIMIT 1 只留一行,要拿全一个父记录的子记录,LIMIT 必须作用在父表上,和上面的分页是同一个问题。
find_related:从一个 Model 出发
CHANGELOG 推荐的替代办法是 ModelTrait::find_related:
fn find_related<R>(&self, _: R) -> Select<R>
where
R: EntityTrait,
Self::Entity: Related<R>,
{
<Self::Entity as Related<R>>::find_related().belongs_to(self)
}
Related::find_related 是 Select::<R>::new().join_join_rev(JoinType::InnerJoin, Self::to(), Self::via()):从 R 出发,INNER JOIN 回自己的表(多对多再经过中间表)。belongs_to(self) 就是上一页讲的按 self 的主键加过滤,条件写在自己的表上。related.rs 的第四段:
let ann = author::Entity::find_by_id(1).one(&db).await?.ok_or(DbErr::Custom("no ann".into()))?;
let posts = ann.find_related(post::Entity).all(&db).await?;
println!("{:?}", posts.iter().map(|p| &p.title).collect::<Vec<_>>());
let p4 = posts[0].clone();
let a = p4.find_related(author::Entity).one(&db).await?;
println!("{:?}", a.map(|a| a.name));
INFO sqlx::query: summary="SELECT \"author\".\"id\", \"author\".\"name\" FROM …" db.statement="\n\nSELECT \"author\".\"id\", \"author\".\"name\" FROM \"author\" WHERE \"author\".\"id\" = $1 LIMIT $2\n" rows_affected=1 rows_returned=1 elapsed=561.834µs elapsed_secs=0.000561834
INFO sqlx::query: summary="SELECT \"post\".\"id\", \"post\".\"title\", \"post\".\"likes\", …" db.statement="\n\nSELECT \"post\".\"id\", \"post\".\"title\", \"post\".\"likes\", \"post\".\"author_id\" FROM \"post\" INNER JOIN \"author\" ON \"author\".\"id\" = \"post\".\"author_id\" WHERE \"author\".\"id\" = $1\n" rows_affected=3 rows_returned=3 elapsed=561.625µs elapsed_secs=0.000561625
["a1", "a2", "a3"]
INFO sqlx::query: summary="SELECT \"author\".\"id\", \"author\".\"name\" FROM …" db.statement="\n\nSELECT \"author\".\"id\", \"author\".\"name\" FROM \"author\" INNER JOIN \"post\" ON \"post\".\"author_id\" = \"author\".\"id\" WHERE \"post\".\"id\" = $1 LIMIT $2\n" rows_affected=1 rows_returned=1 elapsed=564.125µs elapsed_secs=0.000564125
Some("ann")
两个方向都是两张表的 INNER JOIN。查 ann 的文章,本来 WHERE "post"."author_id" = 1 就够了,sea-orm 却 join 了 author,条件写在 "author"."id" 上;查 a1 的作者,手里已经有 author_id,照样 join 回 post、按 "post"."id" 过滤。原因是 find_related 对每种关系只用一种写法:belongs_to 只认识 self 的主键,不知道对面哪一列存着它,所以必须把 self 的表 join 进来;同一套代码对多对多(self 的主键只在中间表的另一头)也成立。代价是多一次 join,主键上的 join 在 Postgres 里通常很便宜,但它不是最简的 SQL。
想要单表查询,就自己写过滤:post::Entity::find().filter(post::Column::AuthorId.eq(ann.id))。
has_related:EXISTS 子查询
2.0 新加的 has_related 回答「有没有满足条件的关联记录」,不 join、不取对面的列:
pub fn has_related<R, C>(mut self, _: R, condition: C) -> Self
where
R: EntityTrait,
E: Related<R>,
C: IntoCondition,
{
let mut to = None;
let mut condition = Condition::all().add(condition.into_condition());
condition = condition.add(if let Some(via) = E::via() {
to = Some(E::to());
via
} else {
E::to()
});
let mut subquery = R::find()
.select_only()
.expr(Expr::cust("1"))
.filter(condition)
.into_query();
if let Some(to) = to {
// join the junction table
subquery.inner_join(to.from_tbl.clone(), to);
}
self.query.cond_where(Expr::exists(subquery));
self
}
子查询是 SELECT 1 FROM R WHERE 调用方的条件 AND 关联条件,关联条件就是 RelationDef 转成的 Condition(impl From<RelationDef> for Condition,见 关系),它引用外层的表,所以这是一个关联子查询;多对多时关联条件是 via(自己到中间表),中间表再 INNER JOIN 到 R。related.rs 的第五段:
let names: Vec<String> = author::Entity::find()
.has_related(post::Entity, post::Column::Likes.gte(40))
// ...
let titles: Vec<String> = post::Entity::find()
.has_related(tag::Entity, tag::Column::Name.eq("sql"))
// ...
INFO sqlx::query: summary="SELECT \"author\".\"id\", \"author\".\"name\" FROM …" db.statement="\n\nSELECT \"author\".\"id\", \"author\".\"name\" FROM \"author\" WHERE EXISTS(SELECT 1 FROM \"post\" WHERE \"post\".\"likes\" >= $1 AND \"author\".\"id\" = \"post\".\"author_id\")\n" rows_affected=2 rows_returned=2 elapsed=700.959µs elapsed_secs=0.000700959
["cid", "dee"]
INFO sqlx::query: summary="SELECT \"post\".\"id\", \"post\".\"title\", \"post\".\"likes\", …" db.statement="\n\nSELECT \"post\".\"id\", \"post\".\"title\", \"post\".\"likes\", \"post\".\"author_id\" FROM \"post\" WHERE EXISTS(SELECT 1 FROM \"tag\" INNER JOIN \"post_tag\" ON \"post_tag\".\"tag_id\" = \"tag\".\"id\" WHERE \"tag\".\"name\" = $1 AND \"post\".\"id\" = \"post_tag\".\"post_id\")\n" rows_affected=2 rows_returned=2 elapsed=842.667µs elapsed_secs=0.000842667
["c1", "a2"]
和用 inner_join 加 filter 比,EXISTS 不会因为一个作者有两篇高赞文章就把他返回两次,不需要 distinct;结果仍是 Select<E>,后面可以接分页、find_with_related。返回的作者没有固定顺序(["c1", "a2"] 不是 id 序),要排序得自己加。
has_related 第二行的 Condition::all().add(..) 有来历。src/bin/has_any.rs 传一个 Condition::any():
let names: Vec<String> = author::Entity::find()
.has_related(
post::Entity,
Condition::any()
.add(post::Column::Likes.gte(45))
.add(post::Column::Title.eq("d1")),
)
// ...
INFO sqlx::query: summary="SELECT \"author\".\"id\", \"author\".\"name\" FROM …" db.statement="\n\nSELECT \"author\".\"id\", \"author\".\"name\" FROM \"author\" WHERE EXISTS(SELECT 1 FROM \"post\" WHERE (\"post\".\"likes\" >= $1 OR \"post\".\"title\" = $2) AND \"author\".\"id\" = \"post\".\"author_id\")\n" rows_affected=2 rows_returned=2 elapsed=3.666583ms elapsed_secs=0.003666583
["cid", "dee"]
调用方的 OR 组被括起来,再和关联条件 AND。
has_related 曾把关联条件 OR 进了调用方的条件has_related 在 2.0.0-rc.28 加入,第一版的第二行是 let mut condition = condition.into_condition();,接着直接 condition.add(E::to())。调用方给的是 Condition::any() 时,add 加进的是同一个 OR 组,上面的子查询就成了 likes >= 45 OR title = 'd1' OR author.id = post.author_id,最后一项让每个有文章的作者都满足条件。2.0.0-rc.43 的 release notes(#3126,「Fix has_related condition building with Condition::any()」)这样描述:「When the caller passed a Condition::any() (an OR group), the join condition was folded into that disjunction, so the relation constraint was no longer guaranteed. The caller's condition is now wrapped in Condition::all() first, yielding (caller condition) AND (fk join) regardless of any() / all().」改动只有这一行。
动脑筋答案
with_limit.rs 最后一段的输出:
--- clear_order_by, then order by post.likes desc
DEBUG sea_orm::driver::sqlx_postgres: SELECT "author"."id" AS "A_id", "author"."name" AS "A_name", "post"."id" AS "B_id", "post"."title" AS "B_title", "post"."likes" AS "B_likes", "post"."author_id" AS "B_author_id" FROM "author" LEFT JOIN "post" ON "author"."id" = "post"."author_id" ORDER BY "post"."likes" DESC
INFO sqlx::query: summary="SELECT \"author\".\"id\" AS \"A_id\", …" db.statement="\n\nSELECT \"author\".\"id\" AS \"A_id\", \"author\".\"name\" AS \"A_name\", \"post\".\"id\" AS \"B_id\", \"post\".\"title\" AS \"B_title\", \"post\".\"likes\" AS \"B_likes\", \"post\".\"author_id\" AS \"B_author_id\" FROM \"author\" LEFT JOIN \"post\" ON \"author\".\"id\" = \"post\".\"author_id\" ORDER BY \"post\".\"likes\" DESC\n" rows_affected=7 rows_returned=7 elapsed=576.875µs elapsed_secs=0.000576875
bob []
cid ["c2", "c1"]
dee ["d1"]
ann ["a2", "a3", "a1"]
第一个是 bob。他没有文章,LEFT JOIN 给他的那一行 post.likes 是 NULL,Postgres 的 DESC 默认 NULLS FIRST,这一行排在最前面。之后的行序是 c2(50)、d1(40)、a2(30)、a3(20)、a1(10)、c1(5):cid 的两行一头一尾,中间隔着 dee 和 ann。consolidate_query_result_of 按主键值分组,照样把 c2、c1 归到 cid 名下,顺序是行序 ["c2", "c1"];作者的顺序是各自第一次出现的位置:bob(第 1 行)、cid(第 2 行)、dee(第 3 行)、ann(第 4 行)。这个结果同时说明了两件事:合并不需要相邻,ORDER BY 父表主键只是为了顺序。
小结
left_join/inner_join/reverse_join按Related选连线,只加JOIN,结果仍是E::Model;ON条件的来历在 关系。find_also_related=left_join+select_also:左表列加A_前缀,右表列生成B_列名,每行读成(M, Option<N>),不加ORDER BY。右边用的默认from_query_result_optional是.ok(),右边的解码错误被静默变成None;find_both_related用INNER JOIN,没有这个问题。find_with_related多一个ORDER BY父表主键,all之后按父表主键值用HashMap合并:不要求行相邻,父记录按第一次出现排序,子记录按行序,没有子记录得到空Vec。之后加的order_by只能当第二排序键。find_with_related(..).limit(n)限制的是 join 后的行:父记录可能少于 n 个,最后一个的子记录可能不全,而且看不出来。SelectTwoMany因此只有all,one在 0.9.0 删掉,paginate从没实现;先限制父表、再查子表用 Loader。model.find_related(R)总是INNER JOIN回自己的表、按自己的主键过滤,两个方向都多一次 join。has_related(R, cond)生成WHERE EXISTS(SELECT 1 FROM R WHERE (cond) AND 关联条件),不会重复返回父记录;Condition::any()的括号问题在 2.0.0-rc.43 修好。