Select 与过滤
主线位置:查询主线的两端。
find_by_id(42)造出的Select<E>在发出去之前可以继续加filter、order_by、limit、lock,每个方法改的都是同一个 sea-querySelectStatement的一个字段;.one(&db)先into_model换成Selector<SelectModel<M>>,这一页的后半段讲Selector的类型参数怎么决定一行被读成什么。中间那段(build、sqlx_query、acquire)在 一次 find 的全过程。
flowchart TB find["Entity::find"] --> sel["Select E"] sel -->|"QueryFilter::filter"| wh["where 子句"] sel -->|"QueryOrder::order_by"| ob["orders"] sel -->|"QuerySelect"| other["列 分组 limit lock"] sel -->|"into_model 等"| selector["Selector S"] selector -->|"one / all"| q["query_one / query_all"] q --> rows["QueryResult"] rows -->|"from_raw_query_result"| item["S::Item"]
实验 crate 依赖 sea-orm =2.0.4(sqlx-postgres、runtime-tokio-rustls、macros、with-chrono、with-json、debug-print),平台 M2 Max、macOS、rustc 1.99.0、Postgres 16.14。表是下一页也会用的一组:
CREATE TABLE author (id serial PRIMARY KEY, name text NOT NULL);
CREATE TABLE post (id serial PRIMARY KEY, title text NOT NULL, likes int NOT NULL,
author_id int NOT NULL REFERENCES author(id));
INSERT INTO author (name) VALUES ('ann'), ('bob'), ('cid'), ('dee');
INSERT INTO post (title, likes, author_id) VALUES
('a1', 10, 1), ('a2', 30, 1), ('a3', 20, 1),
('c1', 5, 3), ('c2', 50, 3),
('d1', 40, 4);
实体用 2.0 的 dense 写法:post 有 id、title、likes、author_id 四列,author 有 id、name。下面的 SQL 都取自 sqlx 的语句日志(sqlx_logging_level(Info) 加 sqlx=info 的 subscriber),那是实际发给服务器的文本,参数是 $N;要看参数值时另开 debug-print(subscriber 过滤 sea_orm::driver=debug),它打印的是值内联后的版本。
Select:一个语句加一个类型标记
Select<E> 只有三个字段:
#[derive(Clone, Debug)]
pub struct Select<E>
where
E: EntityTrait,
{
pub(crate) query: SelectStatement,
pub(crate) entity: PhantomData<E>,
pub(crate) linked_index: usize,
}
query 是 sea-query 的语句树,字段就是 SQL 的各个子句(见 语句是一棵树);entity 不占空间,只让类型系统记住「最后读成 E::Model」;linked_index 给 left_join_linked 数别名 r0、r1 用。Select::new 往 selects 里放实体的每一列、往 from 里放表名(一次 find 的全过程 第 1 节),之后的所有方法都在改 query。
这些方法不是 Select 自己的,而是三个 trait 的默认方法:QueryFilter、QuerySelect、QueryOrder。每个 trait 只要求实现一个 query(&mut self),返回语句的可变引用,其余方法全靠它。impl_query_trait! 给 Select、SelectTwo、SelectTwoMany、SelectTwoRequired 各实现一遍,executor/select/ 下的 SelectThree 到 SelectSix 也用同一个宏,所以 find_also_related 之后照样能 filter、order_by、limit。QuerySelect 和 QueryOrder 的 query() 写死返回 &mut SelectStatement;QueryFilter 返回关联类型 Self::QueryStatement(约束是 sea-query 的 ConditionalStatement),因为 UpdateMany、DeleteMany 也实现了它:update_many().filter(..) 和 find().filter(..) 是同一个方法。
QueryFilter:filter 就是 cond_where
fn filter<F>(mut self, filter: F) -> Self
where
F: IntoCondition,
{
self.query().cond_where(filter.into_condition());
self
}
IntoCondition 对 Expr(post::Column::Likes.gte(10) 的结果)和 Condition 都有实现。sea-query 的 cond_where 每次调用都把新条件 AND 到已有条件上,只有 Condition::any() 产生 OR,这条规则在 语句是一棵树 的「Condition 与 ConditionHolder」一节;eq、starts_with、is_in 各生成什么在 列与值。src/bin/select_sql.rs 的第一段:
let rows = post::Entity::find()
.filter(post::Column::Likes.gte(10))
.filter(
Condition::any()
.add(post::Column::Title.starts_with("a"))
.add(post::Column::AuthorId.eq(4)),
)
.order_by_desc(post::Column::Likes)
.order_by(post::Column::Id, Order::Asc)
.limit(2)
.offset(1)
.all(&db)
.await?;
for r in &rows {
println!("{} {} {}", r.id, r.title, r.likes);
}
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 \"post\".\"likes\" >= $1 AND (\"post\".\"title\" LIKE $2 OR \"post\".\"author_id\" = $3) ORDER BY \"post\".\"likes\" DESC, \"post\".\"id\" ASC LIMIT $4 OFFSET $5\n" rows_affected=2 rows_returned=2 elapsed=4.838916ms elapsed_secs=0.004838916
2 a2 30
3 a3 20
两次 filter 之间是 AND,第二次传进去的 any 组自带括号;五个值全是参数,LIMIT 和 OFFSET 也不例外。满足条件的是 a1(10)、a2(30)、a3(20)、d1(40),按 likes 倒序是 d1、a2、a3、a1,跳过一行取两行,得到 a2、a3。
QueryFilter 另外两个方法也只是 filter 的循环:belongs_to(&model) 对 model 所属实体的每个主键列加一个 col.eq(model.get(col));belongs_to_tbl_alias 是同样的条件,列写在一个别名下。它们是下一页 find_related / find_linked 的最后一步。
QueryOrder:只追加,不替换
QueryOrder::order_by 和 order_by_asc / order_by_desc 都调 sea-query 的 order_by_expr,往 orders 这个 Vec 末尾追加一项,上面的输出里先写的 likes DESC 排在前面。trait 上没有「换掉排序」的方法,要清空得绕到底层的语句:QueryTrait::query(&mut select).clear_order_by()。下一页会用到这一点:find_with_related 在构造时就塞进了一个 ORDER BY,之后写的排序只能排在它后面。order_by_with_nulls 多收一个 NullOrdering,写成 NULLS FIRST / NULLS LAST。Select 自己还有 order_by_id,对每个主键列追加一项。
QuerySelect:列、分组、LIMIT
Select::new 已经列好了实体的每一列。要换成别的列,先 select_only(就是 clear_selects),再逐个加:
fn column<C>(mut self, col: C) -> Self
where
C: ColumnTrait,
{
self.query().expr(col.into_select_expr());
self
}
// ...
fn column_as<C, I>(mut self, col: C, alias: I) -> Self
where
C: ColumnAsExpr,
I: IntoIdentity,
{
self.query().expr(SelectExpr {
expr: col.into_column_as_expr(),
alias: Some(SeaRc::new(alias.into_identity())),
window: None,
});
self
}
column 用的 into_select_expr 和 Select::new 列每一列时是同一个函数,写出来是带表名的 "post"."title"。column_as 收 ColumnAsExpr,列和任意 Expr 都行;expr_as 收一个 Expr 加别名;group_by、having、distinct 各对应一个子句。select_sql.rs 的第二、三段,结果用 into_tuple 按位置读出来(into_tuple 见本页最后一节):
let rows: Vec<(i32, i64, Option<i64>)> = post::Entity::find()
.select_only()
.column(post::Column::AuthorId)
.expr_as(Expr::col(post::Column::Id).count(), "n")
.expr_as(Func::sum(Expr::col(post::Column::Likes)), "total")
.group_by(post::Column::AuthorId)
.having(Expr::col(post::Column::Id).count().gt(1))
.order_by_asc(post::Column::AuthorId)
.into_tuple()
.all(&db)
.await?;
println!("{rows:?}");
// ...
let rows: Vec<String> = post::Entity::find()
.select_only()
.column_as(post::Column::Title, "t")
.distinct()
.filter(post::Column::AuthorId.eq(3))
.order_by_asc(post::Column::Title)
.into_tuple()
.all(&db)
.await?;
println!("{rows:?}");
--- 2. select_only + column + expr_as + group_by + having, into_tuple
INFO sqlx::query: summary="SELECT \"post\".\"author_id\", COUNT(\"id\") AS …" db.statement="\n\nSELECT \"post\".\"author_id\", COUNT(\"id\") AS \"n\", SUM(\"likes\") AS \"total\" FROM \"post\" GROUP BY \"post\".\"author_id\" HAVING COUNT(\"id\") > $1 ORDER BY \"post\".\"author_id\" ASC\n" rows_affected=2 rows_returned=2 elapsed=974.333µs elapsed_secs=0.000974333
[(1, 3, Some(60)), (3, 2, Some(55))]
--- 3. column_as + distinct
INFO sqlx::query: summary="SELECT DISTINCT \"post\".\"title\" AS …" db.statement="\n\nSELECT DISTINCT \"post\".\"title\" AS \"t\" FROM \"post\" WHERE \"post\".\"author_id\" = $1 ORDER BY \"post\".\"title\" ASC\n" rows_affected=2 rows_returned=2 elapsed=1.041708ms elapsed_secs=0.001041708
["c1", "c2"]
COUNT 和 SUM(int) 在 Postgres 里都是 bigint,所以元组里写 i64;SUM 在输入全是 NULL 时返回 NULL,这里写成 Option<i64> 留了这个余地。
Expr::col 不带表名
第二段里 COUNT("id")、SUM("likes") 没有表名,column(..) 写出的 "post"."author_id" 却有。差别在入口:ColumnTrait 的方法(eq、into_expr、into_select_expr)都经过 as_column_ref,得到 (实体名, 列名);Expr::col(post::Column::Id) 走的是 sea-query 的 IntoColumnRef,Column 在那里只是一个 Iden,只有列名。单表查询无所谓,一 join 就出错(src/bin/expr_col.rs):
let unqualified: Result<Vec<(String, i64)>, DbErr> = author::Entity::find()
.inner_join(post::Entity)
.select_only()
.column(author::Column::Name)
.expr_as(Expr::col(post::Column::Id).count(), "n")
.group_by(author::Column::Name)
.into_tuple()
.all(&db)
.await;
println!("Expr::col(Column): {unqualified:?}");
// 第二次把 Expr::col(post::Column::Id) 换成 Expr::col((post::Entity, post::Column::Id)),
// 再加 order_by_asc(author::Column::Name)
Expr::col(Column): Err(Query(SqlxError(Database(PgDatabaseError { severity: Error, code: "42702", message: "column reference \"id\" is ambiguous", detail: None, hint: None, position: Some(Original(31)), where: None, schema: None, table: None, column: None, data_type: None, constraint: None, file: Some("parse_relation.c"), line: Some(831), routine: Some("scanRTEForColumn") }))))
Expr::col((Entity, Column)): [("ann", 3), ("cid", 2), ("dee", 1)]
author 和 post 都有 id,Postgres 拒绝猜。写成元组 (post::Entity, post::Column::Id) 就带上了表名。
枚举列与 column_as
column 和 column_as 对枚举列还多做一件事:读 Postgres enum 时加 CAST(.. AS "text")(为什么要转见 ActiveEnum 与 ValueType)。expr_as(Expr::col(..)) 绕过了这一步。用一个 db_type = "Enum" 的 ticket.status 列,不连库,build(DbBackend::Postgres).sql(src/bin/enum_column_as.rs,这里没有参数,和实际发出的文本一致):
find(): SELECT "ticket"."id", CAST("ticket"."status" AS "text") FROM "ticket"
column_as: SELECT CAST("ticket"."status" AS "text") AS "s" FROM "ticket"
expr_as(col): SELECT "status" AS "s" FROM "ticket"
第三种取回来的是 ticket_status 类型的值,读成 String 会报 sqlx 的 mismatched types。
column_as 收 ColumnAsExpr 是 1.1.9 才改的column_as 原来收的是 IntoSimpleExpr,列被原样写进 SQL,Postgres enum 列加了别名就以 enum 类型返回。#1920(「column_as() breaks PostgreSQL enum」)报的就是这个:join 里对 enum 列 column_as 再 into_model,报 mismatched types; Rust type core::option::Option<alloc::string::String> (as SQL type TEXT) is not compatible with SQL type Role。#2551(「QuerySelect::column_as method cast ActiveEnum column expression as TEXT」)加了「New ColumnAsExpr trait: extending [IntoSimpleExpr] to support casting ActiveEnum as TEXT in select expression」,列走 impl ColumnAsExpr for C 里的 select_as,任意 Expr 原样通过,所以 PR 说这个签名变化「should be backward compatible」。首次发布于 1.1.9,CHANGELOG 记为「QuerySelect::column_as method cast ActiveEnum column」。
2.0.4 新加的 select_except 是 select_only 的反面:clear_selects 之后把除了给定列以外的每一列重新列一遍。它是 Select 的方法,不在 QuerySelect 上,所以 find_also_related 之后用不了;结果仍然按完整的 Model 读:
let rows = post::Entity::find()
.select_except([post::Column::Title])
.filter(post::Column::Id.eq(1))
.all(&db)
.await;
println!("{rows:?}");
INFO sqlx::query: summary="SELECT \"post\".\"id\", \"post\".\"likes\", \"post\".\"author_id\" …" db.statement="\n\nSELECT \"post\".\"id\", \"post\".\"likes\", \"post\".\"author_id\" FROM \"post\" WHERE \"post\".\"id\" = $1\n" rows_affected=1 rows_returned=1 elapsed=456.333µs elapsed_secs=0.000456333
Err(Type("Missing value for column 'title'"))
title 不可空,Model 里没有地方表示「没取」,报的是 列与值 里讲过的 Missing value for column。源码注释也写明「only Option<_> columns can be excluded」;要真正少取列又不出错,读进一个只有那几个字段的结构体(部分列与自定义结果)。
limit 和 offset
limit 和 offset 收 Into<Option<u64>>:
fn limit<T>(mut self, limit: T) -> Self
where
T: Into<Option<u64>>,
{
if let Some(limit) = limit.into() {
self.query().limit(limit);
} else {
self.query().reset_limit();
}
self
}
传 None 是清掉之前设的值,于是「按条件决定要不要分页」可以写成一条链 .limit(page_size),page_size: Option<u64>。值是 u64,在 sea-query 里是 Value::BigUnsigned,发出去是一个 int8 参数(一次 find 的全过程 的抓包)。
LIMIT 和 OFFSET 各是几?拿回来的是哪一篇? let p = post::Entity::find()
.order_by_asc(post::Column::Id)
.limit(3)
.offset(2)
.one(&db)
.await?;
lock:FOR UPDATE 及其变体
QuerySelect 的四个锁方法都只是转给 sea-query:
fn lock(mut self, lock_type: LockType) -> Self {
self.query().lock(lock_type);
self
}
/// Select lock shared
fn lock_shared(mut self) -> Self {
self.query().lock_shared();
self
}
/// Select lock exclusive
fn lock_exclusive(mut self) -> Self {
self.query().lock_exclusive();
self
}
/// Row locking with behavior (if supported).
///
/// See [`SelectStatement::lock_with_behavior`](https://docs.rs/sea-query/*/sea_query/query/struct.SelectStatement.html#method.lock_with_behavior).
fn lock_with_behavior(mut self, r#type: LockType, behavior: LockBehavior) -> Self {
self.query().lock_with_behavior(r#type, behavior);
self
}
LockType 有 Update、NoKeyUpdate、Share、KeyShare 四种,lock_phrase 把它们写成 FOR UPDATE、FOR NO KEY UPDATE、FOR SHARE、FOR KEY SHARE;lock_shared 是 Share,lock_exclusive 是 Update。LockBehavior 的 Nowait / SkipLocked 由 prepare_select_lock 接在后面。SQLite 的 builder 把 prepare_select_lock 覆盖成空函数,锁子句在那里被静默丢掉。
行锁只在事务里有意义:不在事务里时,语句一结束锁就释放了。src/bin/lock.rs 开两个事务(池子 2 条连接),A 锁住 post 1,B 分别用 SKIP LOCKED 和 NOWAIT 去拿:
let a = db.begin().await?;
let p1 = post::Entity::find_by_id(1).lock_exclusive().one(&a).await?;
println!("A got {:?}", p1.map(|p| p.id));
let b = db.begin().await?;
let ids: Vec<i32> = post::Entity::find()
.lock_with_behavior(LockType::Update, LockBehavior::SkipLocked)
.order_by_asc(post::Column::Id)
.all(&b)
.await?
.into_iter()
.map(|p| p.id)
.collect();
println!("B got {ids:?}");
let r = post::Entity::find()
.filter(post::Column::Id.eq(1))
.lock_with_behavior(LockType::Share, LockBehavior::Nowait)
.all(&b)
.await;
println!("B result: {r:?}");
b.rollback().await?;
a.rollback().await?;
--- A: lock post 1
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 \"post\".\"id\" = $1 LIMIT $2 FOR UPDATE\n" rows_affected=1 rows_returned=1 elapsed=2.250875ms elapsed_secs=0.002250875
A got Some(1)
--- B: SKIP LOCKED
INFO sqlx::query: summary="SET search_path = \"so_select1\"" db.statement="" rows_affected=0 rows_returned=0 elapsed=386.25µs elapsed_secs=0.00038625
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\" ORDER BY \"post\".\"id\" ASC FOR UPDATE SKIP LOCKED\n" rows_affected=5 rows_returned=5 elapsed=2.474667ms elapsed_secs=0.002474667
B got [2, 3, 4, 5, 6]
--- B: NOWAIT on post 1
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 \"post\".\"id\" = $1 FOR SHARE NOWAIT\n" rows_affected=0 rows_returned=0 elapsed=743.041µs elapsed_secs=0.000743041
B result: Err(Query(SqlxError(Database(PgDatabaseError { severity: Error, code: "55P03", message: "could not obtain lock on row in relation \"post\"", detail: None, hint: None, position: None, where: None, schema: None, table: None, column: None, data_type: None, constraint: None, file: Some("heapam.c"), line: Some(4832), routine: Some("heap_lock_tuple") }))))
INFO sqlx::query: summary="ROLLBACK" db.statement="" rows_affected=0 rows_returned=0 elapsed=203.167µs elapsed_secs=0.000203167
INFO sqlx::query: summary="ROLLBACK" db.statement="" rows_affected=0 rows_returned=0 elapsed=254.208µs elapsed_secs=0.000254208
锁子句由 prepare_select_lock 写在语句最后。B 中间那条 SET search_path 是池子第二条连接建立时执行的。SKIP LOCKED 跳过了被 A 锁住的 1 号,NOWAIT 不等,直接报 55P03。多个 worker 各自 FOR UPDATE SKIP LOCKED LIMIT n 取任务时,就靠这一点互不阻塞。事务本身(begin、DatabaseTransaction 实现 ConnectionTrait)在 事务。
锁和 join 放在一起有一个限制:FOR UPDATE 不加 OF 时要锁结果里所有表的行,LEFT JOIN 可空的那一侧锁不了。src/bin/lock_join.rs 在事务里对 find_also_related(下一页,LEFT JOIN)加 lock_exclusive(),再试一次只锁 author:
let r = author::Entity::find()
.find_also_related(post::Entity)
.lock_exclusive()
.all(&txn)
.await;
println!("{:?}", r.map(|v| v.len()));
// ...
let mut q = author::Entity::find().find_also_related(post::Entity);
QueryTrait::query(&mut q).lock_with_tables(LockType::Update, [author::Entity]);
let r = q.all(&txn).await;
println!("{:?}", r.map(|v| v.len()));
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\" FOR UPDATE\n" rows_affected=0 rows_returned=0 elapsed=1.5905ms elapsed_secs=0.0015905
Err(Query(SqlxError(Database(PgDatabaseError { severity: Error, code: "0A000", message: "FOR UPDATE cannot be applied to the nullable side of an outer join", detail: None, hint: None, position: None, where: None, schema: None, table: None, column: None, data_type: None, constraint: None, file: Some("initsplan.c"), line: Some(1404), routine: Some("make_outerjoininfo") }))))
INFO sqlx::query: summary="ROLLBACK" db.statement="" rows_affected=0 rows_returned=0 elapsed=149.833µs elapsed_secs=0.000149833
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\" FOR UPDATE OF \"author\"\n" rows_affected=7 rows_returned=7 elapsed=1.624416ms elapsed_secs=0.001624416
Ok(7)
QuerySelect 没有暴露 sea-query 的 lock_with_tables(FOR UPDATE OF ...),要用得通过 QueryTrait::query 拿到底层语句。find_with_related 也是 LEFT JOIN,同样适用。
Selector:同一条 SQL,不同的读法
查询主线上 Select::one 只有一行:self.into_model().one(db).await。into_model 把 query 原样搬进 Selector<S>:
#[derive(Clone, Debug)]
pub struct Selector<S>
where
S: SelectorTrait,
{
pub(crate) query: SelectStatement,
selector: PhantomData<S>,
}
和 Select 一样,又是一个语句加一个类型标记。不同的是标记的意义:Select<E> 记的是「查哪个实体」,Selector<S> 记的是「一行怎么读」。读法由 SelectorTrait 规定:
pub trait SelectorTrait {
/// Type produced for each row.
type Item: Sized;
/// Decode one row.
fn from_raw_query_result(res: QueryResult) -> Result<Self::Item, DbErr>;
}
它没有 self,读一行只需要类型信息,所以 S 只活在 PhantomData 里。Select 上的几个 into_* 只是换 S:
| 方法 | S | from_raw_query_result 做什么 | 列怎么对上 |
|---|---|---|---|
into_model::<M>()(one / all 用 M = E::Model) | SelectModel<M> | M::from_query_result(&res, "") | 按字段名,前缀为空 |
into_json() | SelectModel<JsonValue> | JsonValue 的 FromQueryResult | 每一列一个键 |
into_tuple::<T>() | SelectGetableTuple<T> | T::try_get_many_by_index(&res) | 按位置,第 0、1、…… 列 |
into_values::<T, C>() | SelectGetableValue<T, C> | T::try_get_many(&res, "", &cols) | 按 C 这个枚举的每个变体名 |
find_also_related 之后 | SelectTwoModel<M, N> | M 用前缀 A_、N 用前缀 B_ | 见 join 与 find_related |
除了 into_partial_model(它先 select_only 再按结构体加列,见 部分列与自定义结果),这些方法都不碰 query。src/bin/selectors.rs 拿同一个 Select 换五种读法:
#[derive(Debug, FromQueryResult)]
struct TitleLikes {
title: String,
likes: i32,
}
#[derive(Copy, Clone, Debug, EnumIter, DeriveIden)]
enum Pick {
Title,
Likes,
}
// ...
let base = || post::Entity::find().filter(post::Column::AuthorId.eq(3)).order_by_asc(post::Column::Id);
println!("--- Model");
println!("{:?}", base().all(&db).await?);
println!("--- into_model::<TitleLikes>");
println!("{:?}", base().into_model::<TitleLikes>().all(&db).await?);
println!("--- into_tuple::<(i32, String)>");
println!("{:?}", base().into_tuple::<(i32, String)>().all(&db).await?);
println!("--- into_values::<(String, i32), Pick>");
println!("{:?}", base().into_values::<(String, i32), Pick>().all(&db).await?);
println!("--- into_json");
println!("{:?}", base().into_json().all(&db).await?);
输出(每次的 sqlx 日志与 debug-print 都一样,只留第一次的,其余以 ... 代替):
--- Model
DEBUG sea_orm::driver::sqlx_postgres: SELECT "post"."id", "post"."title", "post"."likes", "post"."author_id" FROM "post" WHERE "post"."author_id" = 3 ORDER BY "post"."id" ASC
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 \"post\".\"author_id\" = $1 ORDER BY \"post\".\"id\" ASC\n" rows_affected=2 rows_returned=2 elapsed=4.519667ms elapsed_secs=0.004519667
[Model { id: 4, title: "c1", likes: 5, author_id: 3 }, Model { id: 5, title: "c2", likes: 50, author_id: 3 }]
--- into_model::<TitleLikes>
...
[TitleLikes { title: "c1", likes: 5 }, TitleLikes { title: "c2", likes: 50 }]
--- into_tuple::<(i32, String)>
...
[(4, "c1"), (5, "c2")]
--- into_values::<(String, i32), Pick>
...
[("c1", 5), ("c2", 50)]
--- into_json
...
[Object {"author_id": Number(3), "id": Number(4), "likes": Number(5), "title": String("c1")}, Object {"author_id": Number(3), "id": Number(5), "likes": Number(50), "title": String("c2")}]
五次发出去的 SQL 一字不差,都取了四列。TitleLikes 只要两列,另外两列照样传回来、被忽略;into_tuple::<(i32, String)> 读的是第 0、1 列,也就是 id 和 title,和列名无关;into_values 按 Pick 的变体名 title、likes 去找列。要让 SQL 也只取需要的列,得自己 select_only().column(..),或者用 into_partial_model。
Selector 像仓库发出的一个包裹(query),上面贴着一张拆箱单(S)。包裹里装什么在打包时就定了(filter、column 改的都是包裹),拆箱单只规定收到以后怎么拆:按名字找(SelectModel)、按格子顺序拿(SelectGetableTuple)、全部倒进一个袋子(into_json)。换拆箱单(into_tuple 换成 into_values)不会让仓库少装一件东西。
比方到这里为止:into_partial_model 是例外,它在贴单子的同时把包裹重新打包了一遍(select_only 加列)。
Selector 的执行方法只有几行。Selector::one 和 all:
pub async fn one<C>(mut self, db: &C) -> Result<Option<S::Item>, DbErr>
where
C: ConnectionTrait,
{
self.query.limit(1);
let row = db.query_one(&self.query).await?;
match row {
Some(row) => Ok(Some(S::from_raw_query_result(row)?)),
None => Ok(None),
}
}
// ...
pub async fn all<C>(self, db: &C) -> Result<Vec<S::Item>, DbErr>
where
C: ConnectionTrait,
{
db.query_all(&self.query)
.await?
.into_iter()
.map(|row| S::from_raw_query_result(row))
.try_collect()
}
one 无条件 limit(1),覆盖之前设的任何 LIMIT,OFFSET 不动。all 先把所有行取回内存,再逐行解码,任何一行失败整个 all 就是 Err(try_collect)。2.0.2 加的 require_one 是 one 加 ok_or(RecordNotFound),消息是 None of the models match the query。逐行流式读的 stream 在 2.0 里要开 stream feature。
动脑筋答案
src/bin/quiz_one.rs,subscriber 只开 sea_orm::driver=debug,看 debug-print 打出的、值已内联的语句:
let p = post::Entity::find()
.order_by_asc(post::Column::Id)
.limit(3)
.offset(2)
.one(&db)
.await?;
println!("{p:?}");
DEBUG sea_orm::driver::sqlx_postgres: SELECT "post"."id", "post"."title", "post"."likes", "post"."author_id" FROM "post" ORDER BY "post"."id" ASC LIMIT 1 OFFSET 2
Some(Model { id: 3, title: "a3", likes: 20, author_id: 1 })
LIMIT 1 OFFSET 2。.limit(3) 被 Selector::one 里的 self.query.limit(1) 覆盖了,offset(2) 原样保留,于是跳过 id 1、2,拿到 id 3。实际发给服务器的是 LIMIT $1 OFFSET $2 两个 int8 参数,值是 1 和 2;debug-print 的这行是把它们内联后打印的。
小结
Select<E>是一个SelectStatement加PhantomData<E>;QueryFilter/QuerySelect/QueryOrder的方法都通过query()改这个语句,SelectTwo等和UpdateMany/DeleteMany(只有QueryFilter)共用它们。- 多次
filter用AND连接,Condition::any()才是OR;order_by只追加;limit(None)清掉LIMIT;LIMIT/OFFSET都是绑定参数。 column/column_as写出带表名的列,枚举列加CAST AS text;Expr::col(Column)只有列名,join 时会42702 column reference "id" is ambiguous。select_except漏掉非空列报Missing value for column。lock_exclusive/lock_with_behavior生成FOR UPDATE [SKIP LOCKED | NOWAIT],放在LIMIT后面;对LEFT JOIN加FOR UPDATE报0A000,要FOR UPDATE OF得用底层的lock_with_tables。Selector<S>也是语句加类型标记,SelectorTrait::from_raw_query_result决定一行读成什么;into_model/into_tuple/into_values/into_json发出完全相同的 SQL,只有into_partial_model改列。Selector::one无条件limit(1),覆盖已设的LIMIT、保留OFFSET;all全部取回再解码,一行出错整体失败。