Rust in Everything
基于 sea-orm 2.0.4

Select 与过滤

主线位置:查询主线的两端。find_by_id(42) 造出的 Select<E> 在发出去之前可以继续加 filter、order_by、limit、lock,每个方法改的都是同一个 sea-query SelectStatement 的一个字段;.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

QueryFilter::filter:

    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。

📝NOTE
冷知识: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 的全过程 的抓包)。

❗IMPORTANT
动脑筋:下面这条查询发出去的 LIMIT 和 OFFSET 各是几?拿回来的是哪一篇?
  let p = post::Entity::find()
    .order_by_asc(post::Column::Id)
    .limit(3)
    .offset(2)
    .one(&db)
    .await?;
平台:M2 Max、macOS、rustc 1.99.0、Postgres 16.14,数据见页首。答案在页末。

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:

方法Sfrom_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。

💡TIP
打个比方:同一个包裹,换一张拆箱单

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 全部取回再解码,一行出错整体失败。

💬 关联讨论

发起讨论
加载中...