SQLx 源码深度解析

第9章 Query 与 QueryAs:最小原语与查询生成链

作者 杨艺韬 · 12,744 字 · 发布于 · 更新于

Query 只有四个字段。用户 API 的全部链式体验——.bind() 一路串到 .fetch_one()——都建在这四个字段上,连 encode 失败都被延迟到下一步才报。

本章要点

  • Query<'q, DB, A>(sqlx-core/src/query.rs:17-22)是 sqlx 用户 API 的核心类型——四个字段:statement(SQL 或 cached Statement)、arguments(参数集合 or 错误)、database(PhantomData)、persistent(是否缓存预处理)。
  • query() 顶层函数(sqlx-core/src/query.rs:655-666)返回初始化好的空 Query——所有字段默认空、persistent = true。用户链式 .bind().bind()...fetch_*(&pool) 消化。
  • .bind(value) 消化进 Arguments——失败延迟:如果 encode 失败,错误存进 arguments: Option<Result<A, BoxDynError>> 的 Err 分支,下一次 get_arguments 才拒绝。这条延迟错误是用户体验的细节。
  • persistent(bool) 只对支持 statement cache 的 DB 可见——通过 where DB: HasStatementCache 守护;0.8.6 里三家 in-tree 驱动都实现了这个 marker,所以三家下这个方法都存在,真正被挡住的是不实现它的第三方驱动(第 3 章 §3.9 讨论过)。
  • 四个 fetch 方法(fetch / fetch_all / fetch_one / fetch_optional)+ execute(sqlx-core/src/query.rs:184-297)全部是对 Executor trait 的薄包装——executor.fetch(self) 而已。Query 本身不承担 I/O,只承担类型组装。
  • QueryAs<'q, DB, O, A>(sqlx-core/src/query_as.rs:19-22)= Query<'q, DB, A> + PhantomData<O>。通过 O: for<'r> FromRow<'r, DB::Row> 的 where bound 把 row 自动映射到 O。
  • QueryScalar<'q, DB, O, A>(sqlx-core/src/query_scalar.rs:19-21)= QueryAs<'q, DB, (O,), A> 的包装——利用 (T,) 的 tuple FromRow blanket impl(第 8 章 §8.11)拿第一列值。
  • #[must_use = "query must be executed..."] 标在 Query / QueryAs / QueryScalar / Map 四个类型上——构造了但没执行直接丢弃会编译器警告,提前发现"忘了执行"的 bug。

9.1 问题引入:从 "query(sql)" 到 "row 流"

一条典型用户代码:

let count: i64 = sqlx::query_scalar("SELECT COUNT(*) FROM users WHERE active = $1")
    .bind(true)
    .fetch_one(&pool)
    .await?;

这段代码调了四个链式方法——query_scalar 构造、bind 消化参数、fetch_one 触发执行、await 等待完成。从类型系统角度看,每一步都是类型的精确转换:

  1. query_scalar::<_, i64>("...") → QueryScalar<'_, DB, i64, DB::Arguments<'_>>。
  2. .bind(true) → 同一个 QueryScalar,内部 Arguments 加了一个 bool。
  3. .fetch_one(&pool) → impl Future<Output = Result<i64, Error>>。
  4. .await? → i64。

这里面每一步的类型组装都由 Query / QueryAs / QueryScalar 三个 struct 和 query() / query_as() / query_scalar() / query_statement() 四个函数完成。本章的任务是把这条链的每一处类型转换拆开——看"SQL 字符串 → 可执行 Future" 是如何通过薄薄几百行代码连起来的。

核心观察是:sqlx 的 Query 本身不做 I/O。它只是一个类型容器——装 SQL 字符串、装 Arguments、装 persistent 标志。真正的 I/O 发生在 .fetch_*(&pool).await 里——Query 被 move 给 Executor,最终落到驱动层的 fetch_many 或 fetch_optional 做协议交互(§9.6)。这条"Query 管组装,Executor 管执行"的分层是本章要建立的心智模型。

9.2 Query<'q, DB, A> 结构

sqlx-core/src/query.rs:17-22:

/// A single SQL query as a prepared statement. Returned by [`query()`].
#[must_use = "query must be executed to affect database"]
pub struct Query<'q, DB: Database, A> {
    pub(crate) statement: Either<&'q str, &'q DB::Statement<'q>>,
    pub(crate) arguments: Option<Result<A, BoxDynError>>,
    pub(crate) database: PhantomData<DB>,
    pub(crate) persistent: bool,
}

四个字段、两个 trait 参数、一个生命周期。逐个看:

9.2.1 statement: Either<&'q str, &'q DB::Statement<'q>>

SQL 的两种形态:

  • Either::Left(&str) —— 纯字符串 SQL,最常见。
  • Either::Right(&Statement) —— 已经 prepare 过的 Statement 对象。

第二种形态用在"反复执行同一条 SQL"场景——你先 conn.prepare("...").await? 拿到 Statement,再 query_statement(&stmt).bind(x).fetch...。prepared statement 跨多次执行复用,省 Parse 阶段的开销。

生命周期 'q 把 SQL 字符串和 Statement 的借用锁在一起——Query 活多久,底层字符串就得活多久。

9.2.2 arguments: Option<Result<A, BoxDynError>>

三状态表达:

  • None —— 参数已经被 take_arguments 取走(移交给 executor)。
  • Some(Ok(A)) —— 参数完好,可继续 bind 或执行。
  • Some(Err(e)) —— bind 过程中 encode 失败,错误延迟到执行时才报。

这个复合类型表达的是参数生命周期的三个阶段 + 延迟错误。后面 §9.4 讨论 bind 时会展开为什么不直接 panic。

9.2.3 database: PhantomData<DB>

类型标记占位。Query 自身不持有 DB 的任何值——DB 是 Postgres / MySql / Sqlite 这种零大小类型。但类型系统需要 Query<'q, DB, A> 的 DB 参数出现在结构里才能参与类型推导。PhantomData<DB> 解决这个问题:零字节占位、零运行时开销、只是类型级标记。

第 3 章讨论过 Database trait 实现者是"类型 token"——PhantomData 是承载这些 token 的标准手法。

9.2.4 persistent: bool

执行时是否把预处理语句放进连接的语句缓存。默认 true——sqlx 认为"同一条 SQL 大概率会被多次执行"。false 用于一次性 SQL,语句不进缓存(三家驱动各自怎么处理见 §9.5)。

这个字段独立于 DB 参数存在——但只有 DB: HasStatementCache 的方法(persistent() setter)能修改它。在不实现这个 marker 的第三方驱动下字段还在,却没有公共 setter——永远是构造时的初始值。

9.3 query() 顶层函数

sqlx-core/src/query.rs:655-666:

pub fn query<DB>(sql: &str) -> Query<'_, DB, <DB as Database>::Arguments<'_>>
where DB: Database,
{
    Query {
        database: PhantomData,
        arguments: Some(Ok(Default::default())),
        statement: Either::Left(sql),
        persistent: true,
    }
}

十二行代码。构造一个空 Query——参数集合用 DB::Arguments::default() 空初始化、SQL 直接借用传入的 &str、persistent 默认 true。

注意泛型 DB 必须由调用方指定——sqlx::query::<Postgres>("...") 或更常见的通过类型推导(let q: Query<Postgres, _> = sqlx::query("..."))。用户代码里几乎不会显式指定——sqlx 的通用用法是 pool 的类型决定 DB,所以 query("...").fetch_one(&pg_pool) 会让编译器从 pg_pool: PgPool = Pool<Postgres> 反推 DB = Postgres。

9.3.1 query_with 和 query_with_result

sqlx-core/src/query.rs:670-692 有两个变体函数:

pub fn query_with<'q, DB, A>(sql: &'q str, arguments: A) -> Query<'q, DB, A>
where DB: Database, A: IntoArguments<'q, DB>,
{
    query_with_result(sql, Ok(arguments))
}

pub fn query_with_result<'q, DB, A>(sql: &'q str, arguments: Result<A, BoxDynError>) -> Query<'q, DB, A>
where DB: Database, A: IntoArguments<'q, DB>,
{
    Query { database: PhantomData, arguments: Some(arguments), statement: Either::Left(sql), persistent: true }
}
  • query_with:传入已经构造好的 Arguments(而不是从空开始 bind)。适用于你有一个自定义参数容器、不想走 bind 链的场景。
  • query_with_result:接受 Result<A, BoxDynError>——允许构造时已经有错误(比如你提前尝试 encode 失败)。

这两个函数用户代码里较少用——它们主要服务 query! 宏。宏不走 bind 链:sqlx-macros-core/src/query/args.rs:113-119 先把所有参数 add 进一个 DB::Arguments<'_>、整条链的结果是一个 Result<_, BoxDynError>,然后 sqlx-macros-core/src/query/output.rs:184 把它原样交给 ::sqlx::__query_with_result——也就是这里的 query_with_result。Result 而不是裸 Arguments 正是为了承接 encode 失败(§9.4.2 的延迟错误在宏这条路上同样成立)。

9.4 .bind() 链:Arguments 消化

sqlx-core/src/query.rs:86-100 的 bind 方法是全专栏最精巧的代码之一:

impl<'q, DB: Database> Query<'q, DB, <DB as Database>::Arguments<'q>> {
    pub fn bind<T: 'q + Encode<'q, DB> + Type<DB>>(mut self, value: T) -> Self {
        let Ok(arguments) = self.get_arguments() else {
            return self;
        };

        let argument_number = arguments.len() + 1;
        if let Err(error) = arguments.add(value) {
            self.arguments = Some(Err(format!(
                "Encoding argument ${argument_number} failed: {error}"
            ).into()));
        }

        self
    }
}

几条重要细节:

9.4.1 impl 块限定 A = DB::Arguments<'q>

注意这个 impl 块不是 impl Query<'q, DB, A>——而是 impl Query<'q, DB, DB::Arguments<'q>>——只给默认 Arguments 类型实现 bind。如果你用 query_with 传入自定义参数容器,那个 Query 的 A 是你自定义类型,没有 bind 方法——你得自己塞参数。

这条限制是为了保持 bind 语义清晰——只有"持有 DB::Arguments<'q>"的 Query 才有"添加参数"的操作;换成任何别的 A(自定义容器,或 sqlx-core/src/arguments.rs:54 那个已经赋闲的 ImmutableArguments),bind 这个方法在类型上就整个不存在了。

9.4.2 bind 失败的延迟错误

get_arguments() 检查 self.arguments:

fn get_arguments(&mut self) -> Result<&mut DB::Arguments<'q>, BoxDynError> {
    let Some(Ok(arguments)) = self.arguments.as_mut().map(Result::as_mut) else {
        return Err("A previous call to Query::bind produced an error".to_owned().into());
    };
    Ok(arguments)
}

如果之前有 bind 失败了(arguments 是 Some(Err(...))),get_arguments 返回错误。bind 方法收到错误不 propagate,只是 return self——继续返回原 Query。

然后 arguments.add(value) 如果失败,把错误覆盖进 self.arguments —— 把 Some(Ok(args)) 变成 Some(Err("...failed"))。这条覆盖意味着"后续 bind 全部静默丢弃,错误延迟到 fetch_* 时统一报"。

为什么这样设计?考虑对比方案:

方案 A:bind 立即 panic

pub fn bind(mut self, value: T) -> Self {
    self.arguments.unwrap().add(value).unwrap();  // panic on failure
    self
}

——对链式代码不友好,一处 panic 整链断。

方案 B:bind 返回 Result

pub fn bind(mut self, value: T) -> Result<Self, Error> { ... }

——每次 bind 都要 ? 或 unwrap——链式风格破坏:

sqlx::query(sql).bind(a)?.bind(b)?.bind(c)?.fetch_one(pool).await?

方案 C(sqlx 的选择):延迟错误

sqlx::query(sql).bind(a).bind(b).bind(c).fetch_one(pool).await?

所有错误汇聚到最后一个 ?——链式风格保留、错误仍然被报出(在 fetch 时)、只是报得晚一点。代价是定位失败的 bind 位置略困难——错误信息里的 "Encoding argument $2 failed" 靠位置编号告诉你是第几次 bind 失败。

这条选择是"API 工效优于错误即时性"的典型权衡。sqlx 选了 C(链式优先)——这条决定影响了所有下游用户代码的书写风格。

9.5 persistent 开关

sqlx-core/src/query.rs:123-141 只给 DB: HasStatementCache 实现:

impl<'q, DB, A> Query<'q, DB, A>
where DB: Database + HasStatementCache,
{
    pub fn persistent(mut self, value: bool) -> Self {
        self.persistent = value;
        self
    }
}

作用:默认 true 让 driver 把 prepared statement 缓存在 connection 里,多次执行同 SQL 复用。false 表示这条语句不进缓存,三家驱动的落地方式不同(0.8.6):

  • Postgres:get_or_prepare 先按 SQL 查缓存,命中就直接用——即使本次是 false;未命中时用未命名语句(unnamed statement)Parse,不插入缓存,下一条未命名 Parse 会把它顶掉(sqlx-postgres/src/connection/executor.rs:168-196、30-36,即 [#3863])。
  • MySQL:不查缓存,每次重新预处理(协议里的 COM_STMT_PREPARE),执行完立刻发 StmtClose 包(即 COM_STMT_CLOSE)关掉(sqlx-mysql/src/connection/executor.rs:121-150)。
  • SQLite:不查缓存,编译进连接上唯一的临时槽位,下一条非缓存语句会替换它(sqlx-sqlite/src/connection/mod.rs:581-584)。

另外,参数为 None 的查询(直接把 &str 交给 executor、或 raw_sql,见 §9.23)根本不走预处理:Postgres 发简单查询 Query 消息,MySQL 发 COM_QUERY,SQLite 则把 persistent 强制为 false(sqlx-sqlite/src/connection/executor.rs:31)。而 query("SELECT 1") 即使一次 bind 都没有,参数也是 Some(空 Arguments),照样预处理并缓存。

什么时候用 false?

  1. SQL 只执行一次——例如启动时跑迁移初始化。缓存这条 statement 没意义。
  2. SQL 基数巨大——动态生成的 SQL 有上千种模式。语句缓存是容量固定的 LRU(sqlx-core/src/common/statement_cache.rs:1-14),满了就淘汰;模式太多只会不停淘汰重建(Postgres 每淘汰一条还要额外发一次 Close),得不偿失。
  3. 中间件不兼容——query() 的文档点名 PgBouncer、CockroachDB 这类讲 Postgres 协议的第三方组件可能和透明缓存合不来,出问题先试 .persistent(false)(sqlx-core/src/query.rs:540-542)。此外缓存的 prepared statement 在服务端占会话资源(Postgres 可在 pg_prepared_statements 视图里看到本会话的),长连接上缓存越多占得越多。

0.8.6 里 Postgres / MySQL / SQLite 三家 in-tree 驱动都实现了 HasStatementCache(sqlx-postgres/src/database.rs:40、sqlx-mysql/src/database.rs:38、sqlx-sqlite/src/database.rs:39),所以三家下 persistent 方法都存在。这条 bound 挡住的是不实现该 marker 的第三方驱动——那时 persistent 在编译期就不存在。

9.6 fetch / execute 系列:对 Executor 的薄包装

sqlx-core/src/query.rs:143-298 大片内容是 map / fetch / execute 方法族——除了构造 Map 的 map / try_map,其余全部只做一件事:executor.XXX(self)。

impl<'q, DB, A: Send> Query<'q, DB, A>
where DB: Database, A: 'q + IntoArguments<'q, DB>,
{
    pub async fn execute<'e, 'c: 'e, E>(self, executor: E) -> Result<DB::QueryResult, Error>
    where 'q: 'e, A: 'e, E: Executor<'c, Database = DB>,
    { executor.execute(self).await }

    pub fn fetch<'e, 'c: 'e, E>(self, executor: E) -> BoxStream<'e, Result<DB::Row, Error>> where ...
    { executor.fetch(self) }

    pub async fn fetch_all<'e, 'c: 'e, E>(self, executor: E) -> Result<Vec<DB::Row>, Error> where ...
    { executor.fetch_all(self).await }

    pub async fn fetch_one<'e, 'c: 'e, E>(self, executor: E) -> Result<DB::Row, Error> where ...
    { executor.fetch_one(self).await }

    pub async fn fetch_optional<'e, 'c: 'e, E>(self, executor: E) -> Result<Option<DB::Row>, Error> where ...
    { executor.fetch_optional(self).await }
}

每个方法就是一行调用——把 self 交给 executor 上的同名方法。再往下一层,Executor 里这几个方法都有默认实现,最终只落到 fetch_many / fetch_optional 两个读/执行原语上(sqlx-core/src/executor.rs:37-138;驱动另须实现 prepare_with / describe,共 4 个必实现方法):

Query 上调用 调 Executor 的 默认实现归约到
execute execute execute_many → fetch_many,只留 QueryResult
fetch fetch fetch_many,只留 Row
fetch_all fetch_all fetch → fetch_many,收集成 Vec
fetch_one fetch_one fetch_optional,None 转成 Error::RowNotFound
fetch_optional fetch_optional 必须实现的方法本身

0.8.6 里 Pool、三家 &mut XxxConnection、PgListener、AnyConnection 的 Executor impl 都只写了 fetch_many 和 fetch_optional(外加 prepare_with / describe),没有覆盖上面这些默认方法。

这种"薄包装"设计有两个价值:

  1. 为 Query 带来 "执行行为"——没有这些方法,用户得写 executor.fetch(my_query) 而不是 my_query.fetch(executor)。后者读起来更流畅("对这个查询 fetch" vs "用 executor fetch 这个查询")。
  2. 统一签名——所有 fetch 方法 where 子句里都有 'q: 'e, A: 'e,由 sqlx-core 统一写一次;用户不用重复写这条 bound。

注意 fetch_many / execute_many 被 #[deprecated] 标记(sqlx-core/src/query.rs:195, 224)——原因是只有 SQLite 支持一个 prepared statement 里多条语句,生态方向是让用户用 sqlx::raw_sql() 明确表达多语句。这次软弃用和 raw_sql API 是同一个 PR([#3007],0.7.4)带来的。

9.6.1 execute 返回 QueryResult,其它返回 Row

execute 和 fetch_* 的本质差异:

  • execute 执行但不关心 row——直接返回 QueryResult(rows_affected)。适合 UPDATE / DELETE / INSERT without RETURNING。
  • fetch_* 返回 Row(或 Row 的转换)。适合 SELECT 或带 RETURNING 的 DML。

Postgres 下 INSERT INTO ... RETURNING id 要用 fetch_one 而不是 execute——RETURNING 有返回行,execute 会丢掉。这是新手常见的一个 confusion——记住 "关心返回的行 → fetch / 不关心 → execute"。

9.7 QueryAs<'q, DB, O, A>:映射到 Rust 类型

sqlx-core/src/query_as.rs:19-22:

/// A single SQL query as a prepared statement, mapping results using [`FromRow`].
#[must_use = "query must be executed to affect database"]
pub struct QueryAs<'q, DB: Database, O, A> {
    pub(crate) inner: Query<'q, DB, A>,
    pub(crate) output: PhantomData<O>,
}

QueryAs 包装 Query + 添加 O 输出类型标记。O 是最终的 Rust 类型(通常是你自己的 struct)——通过 O: for<'r> FromRow<'r, DB::Row> bound 绑定 FromRow 转换。

query_as 顶层函数(sqlx-core/src/query_as.rs:342-351):

pub fn query_as<'q, DB, O>(sql: &'q str) -> QueryAs<'q, DB, O, <DB as Database>::Arguments<'q>>
where
    DB: Database,
    O: for<'r> FromRow<'r, DB::Row>,
{
    QueryAs {
        inner: query(sql),
        output: PhantomData,
    }
}

就是把 query() 的结果包进 QueryAs,加一个 PhantomData<O>。

9.7.1 QueryAs 的 fetch 方法

sqlx-core/src/query_as.rs:82-200 的 fetch 系列比 Query 稍复杂——因为每个 row 要经过 O::from_row:

pub async fn fetch_optional<'e, 'c: 'e, E>(self, executor: E) -> Result<Option<O>, Error>
where 'q: 'e, E: 'e + Executor<'c, Database = DB>, DB: 'e, O: 'e, A: 'e,
{
    let row = executor.fetch_optional(self.inner).await?;
    if let Some(row) = row {
        O::from_row(&row).map(Some)
    } else {
        Ok(None)
    }
}

两步:

  1. executor.fetch_optional(self.inner) —— 拿回 Option<DB::Row>。
  2. Some(row) 上跑 O::from_row(&row) —— 把 row 转成 O。

错误合流:如果 executor 的 fetch 失败、或者 from_row 失败,都走 Result 的 Err 分支。

fetch_all / fetch_one / fetch / fetch_many 都是类似模式——拿 row、过 from_row、收集 / 返回。但它们走的 Executor 方法和 Query 不完全一样:fetch_one 是 self.fetch_optional() 再把 None 转成 RowNotFound;fetch 调的是 executor.fetch_many 再滤掉 QueryResult(源码 FIXME 说本该用 executor.fetch(),但改了就是 breaking change,sqlx-core/src/query_as.rs:97-102);fetch_all 则是 self.fetch().try_collect()。QueryScalar 和 Map 的对应方法同理。这条row → O 的映射是 QueryAs 相对 Query 的核心附加功能。

9.7.2 为什么不用 Query::map / try_map?

sqlx-core/src/query.rs:155-180 还有一组 map / try_map 方法返回 Map<'q, DB, F, A>:

pub fn map<F, O>(self, f: F) -> Map<'q, DB, impl FnMut(DB::Row) -> Result<O, Error> + Send, A>
where F: FnMut(DB::Row) -> O + Send, O: Unpin,
{
    self.try_map(move |row| Ok(f(row)))
}

pub fn try_map<F, O>(self, f: F) -> Map<'q, DB, F, A>
where F: FnMut(DB::Row) -> Result<O, Error> + Send, O: Unpin,
{
    Map { inner: self, mapper: f }
}

Map 是"用一个闭包把 Row 映射成 O"的版本——不依赖 FromRow trait。

QueryAs 和 Map 的选择:

  • QueryAs:O 实现了 FromRow(派生或手写)——最常见。
  • Map:给一个闭包、灵活但重复代码——适合一次性特化映射。

query_as().bind().fetch_one vs query().bind().try_map(|row| { ... }).fetch_one——前者简洁,后者灵活。常规业务代码里 QueryAs 更常见。

9.8 QueryScalar<'q, DB, O, A>:取第一列

sqlx-core/src/query_scalar.rs:19-21:

#[must_use = "query must be executed to affect database"]
pub struct QueryScalar<'q, DB: Database, O, A> {
    pub(crate) inner: QueryAs<'q, DB, (O,), A>,
}

QueryScalar 内嵌 QueryAs,输出类型是 (O,) 单元素 tuple——利用第 8 章 §8.11 讨论过的 tuple FromRow blanket impl。然后 fetch 方法把 (O,) 解构出 O。

query_scalar 顶层函数(sqlx-core/src/query_scalar.rs:321-331):

pub fn query_scalar<'q, DB, O>(
    sql: &'q str,
) -> QueryScalar<'q, DB, O, <DB as Database>::Arguments<'q>>
where
    DB: Database,
    (O,): for<'r> FromRow<'r, DB::Row>,
{
    QueryScalar {
        inner: query_as(sql),
    }
}

这个函数的价值在类型体验——let count: i64 = query_scalar("SELECT COUNT(*)...").fetch_one(&pool).await? 直接得到 i64 而不是 (i64,)。省一个字段解构,视觉上更清爽。

query_scalar 只适合单列查询——如果 SQL 返回两列,编译能过但运行时只取第一列。这是约定,不是类型约束。

9.9 Map<'q, DB, F, A>:闭包映射

sqlx-core/src/query.rs:36-39:

#[must_use = "query must be executed to affect database"]
pub struct Map<'q, DB: Database, F, A> {
    inner: Query<'q, DB, A>,
    mapper: F,
}

内嵌 Query + 一个 Row → Result<O, Error> 的闭包。fetch 方法里每条 row 先过闭包再 yield。Map 上没有 bind、没有 execute,也没有 persistent setter——源码注释建议把 .bind() 写在 .try_map() 之前(sqlx-core/src/query.rs:24-34),.persistent(false) 同样只能在 try_map 之前对 Query 调。

Map 和 QueryAs 几乎等价——都是"Query + 输出转换"。差异是:

  • QueryAs:转换由 trait 定义(FromRow),写 query_as::<_, User> 即可。
  • Map:转换由闭包定义,写 query(sql).try_map(|row| User::from_row(&row)) 或更灵活的自定义映射。

典型用例是和 FromRow 不兼容的映射——比如你想按 row 动态决定映射到 UserA 还是 UserB(根据某列值):

sqlx::query("SELECT ... FROM ...")
    .try_map(|row: PgRow| {
        let kind: String = row.try_get("kind")?;
        match kind.as_str() {
            "admin" => AdminUser::from_row(&row).map(Entity::Admin),
            "guest" => GuestUser::from_row(&row).map(Entity::Guest),
            other => Err(Error::Decode(format!("unknown kind: {other}").into())),
        }
    })
    .fetch_all(&pool).await?

#[derive(FromRow)] 表达不了这种条件逻辑——要么给 Entity 手写一个 FromRow impl,要么就地 try_map。

9.10 Execute trait 的四个 impl

第 4 章 §4.5 讲过 Execute trait 有两个基础 impl(&str 和 (&str, Option<Args>))。到本章,Query / QueryAs / QueryScalar / Map 各自也实现了 Execute——这是它们能被 executor 消化的关键(第七个实现者 RawSql 放到 §9.23)。

sqlx-core/src/query.rs:41-73 的 Query 实现:

impl<'q, DB, A> Execute<'q, DB> for Query<'q, DB, A>
where DB: Database, A: Send + IntoArguments<'q, DB>,
{
    fn sql(&self) -> &'q str {
        match self.statement {
            Either::Right(statement) => statement.sql(),
            Either::Left(sql) => sql,
        }
    }

    fn statement(&self) -> Option<&DB::Statement<'q>> {
        match self.statement {
            Either::Right(statement) => Some(statement),
            Either::Left(_) => None,
        }
    }

    fn take_arguments(&mut self) -> Result<Option<DB::Arguments<'q>>, BoxDynError> {
        self.arguments.take().transpose().map(|option| option.map(IntoArguments::into_arguments))
    }

    fn persistent(&self) -> bool { self.persistent }
}

四个方法逐一来自 Query 的四字段——sql / statement 从 Either 取、arguments 从 Option<Result> 取、persistent 直接读。

take_arguments 里的 self.arguments.take() 把 Some(...) 换成 None——参数从 Query 移交到 executor 后 Query 里就空了。这保证"同一个参数不会被消费两次"。

QueryAs / QueryScalar 的 Execute impl 都是 delegate 到内嵌 Query 的对应方法——没有独立逻辑。Map 有一处例外:它的 persistent() 返回的是 self.inner.arguments.is_some(),而不是内层的 persistent 字段(sqlx-core/src/query.rs:320-323,2020 年的 590f97df 引入,v0.9.0 仍是这样)。三家驱动都是先 take_arguments() 再读 persistent(),所以把 Map 直接交给 executor(如 conn.fetch_one(map))时它永远按 false 处理、不进缓存;平时写 map.fetch_one(&mut conn) 不受影响,因为 Map 自己的 fetch 方法交给 executor 的是内层 Query(SQLite 上实测:前者缓存条数不变,后者加一)。四个 impl 共同构成"可执行查询"的完整类型集合。

9.11 #[must_use] 的防御

四个 Query 类型上都有:

#[must_use = "query must be executed to affect database"]

这条 attribute 让构造但未执行的 Query 触发编译器警告:

sqlx::query::<Postgres>("UPDATE users SET active = false WHERE id = $1").bind(42);  // warning!
// 忘了 .execute(&pool).await

(这里显式写 ::<Postgres>,是因为没有后续的 .execute(&pool),编译器推不出 DB,不写会先报 E0283。)实测 1.96.0 给出的是 warning: unused `Query` that must be used,附注 query must be executed to affect database——精准的提示"你忘了 .execute/.fetch"。

这种类型级防御在 Rust 里称为 must_use pattern——常见于 Result(用了 Result 得 unwrap)、Future(Future 没 await 等于不工作)。sqlx 对 Query 加上它是对用户常见疏忽的主动防御——新手容易写一半链忘掉 fetch、写代码时意识不到 "forgotten await",编译器警告把这个 bug 拦在了编译期。

这条细节和第 4 章 §4.5.1 "不为 String 实现 Execute" 是同一类技巧——用类型系统表达 API 意图。成本几乎零(一个 attribute),收益是把一整类"忘了执行"的 bug 挪到编译期。

9.12 四个顶层函数的选择原则

用户代码里每次写查询都要在四个顶层函数里选一个:

函数 返回 适用场景
query(sql) Query<'_, DB, Arguments> 多列查询,row 不映射成 struct;或用 try_map 自定义映射
query_as::<_, T>(sql) QueryAs<'_, DB, T, Arguments> 多列查询,row 映射成带 FromRow 的 struct
query_scalar::<_, T>(sql) QueryScalar<'_, DB, T, Arguments> 单列查询,直接拿 T(COUNT、MAX、name 等)
query_statement(&stmt)(用户侧写 stmt.query()) Query<'_, DB, ...> 已 prepared 的 Statement 反复执行

决策树:

flowchart TD
    A{SQL 结果是 单值 单列?}
    A -->|是| QS["query_scalar::&lt;_, T&gt;(sql)"]
    A -->|否| B{映射成 struct?}
    B -->|是| QA["query_as::&lt;_, Struct&gt;(sql)"]
    B -->|否| C{需要自定义映射逻辑?}
    C -->|是| Q1["query(sql).try_map"]
    C -->|否| Q2["query(sql).fetch → 手动 try_get"]
    A --> D{已 prepared?}
    D -->|是| QST["stmt.query() 即 query_statement"]

在实际代码里选哪个对可读性影响很大——用 query 包单列查询再手动 try_get 比 query_scalar 丑很多;用 query_as 包单列查询要多一个 (T,) 元组也不如 query_scalar 直接。精准选对函数 = 少一层样板代码。

9.13 .fetch_one / .fetch_optional 的边界行为

关于单行查询的边界行为,sqlx 有几条容易踩的细节:

fetch_one 对 "0 行" 返回 Error::RowNotFound:

let u: User = sqlx::query_as(...).fetch_one(&pool).await?;
// 查不到:Error::RowNotFound

fetch_optional 对 "0 行" 返回 Ok(None):

let u: Option<User> = sqlx::query_as(...).fetch_optional(&pool).await?;
// 查不到:Ok(None)

两者对"查到多行但只要一行"的处理相同——fetch_one 本来就是 fetch_optional 加一层 None → RowNotFound,都只返回第一行。多出来的行怎么处理看驱动:Postgres 的 fetch_optional 会把剩余响应读完再返回(为了不漏掉延迟约束在后面报的错,sqlx-postgres/src/connection/executor.rs:430-440);SQLite 给 worker 传了 Some(1) 的行数上限,取够一行就不再 step;MySQL 拿到第一行就返回,剩余结果包留到下次用这条连接前由 wait_until_ready(sqlx-mysql/src/connection/stream.rs:72-109)读掉丢弃。所以源码文档(sqlx-core/src/query_as.rs:150-159)提醒:最好保证查询本身只返回一行——加 LIMIT 1 或用 PRIMARY KEY 过滤——让数据库可以用更优的查询计划。

fetch_one 和 fetch_optional 的 API 选择取决于业务语义:

  • "这行必须存在,找不到算错误"——fetch_one,后面 ? 让错误冒泡。
  • "这行可能不存在,找不到走默认分支"——fetch_optional,后面 .map 或 .unwrap_or_default()。

混用会产生笨重的代码:

// 不推荐:fetch_one 套 result.ok() 转 Option
let u_opt: Option<User> = sqlx::query_as(...).fetch_one(&pool).await.ok();
// 问题:除了 RowNotFound,任何其他 Error(如网络断)也会变 None,隐藏真实故障。

正确分流:fetch_optional 明确"只有'未找到'算 None、其他 Error 仍然冒泡":

let u_opt: Option<User> = sqlx::query_as(...).fetch_optional(&pool).await?;

这条语义差别(fetch_one+ok() vs fetch_optional)在业务逻辑上是错误吞没 vs 正确处理——选对 API 就是选对错误语义。

9.14 本章小结

本章把 sqlx 用户 API 的核心类型链全部拆开:

  1. Query 结构四字段(§9.2)—— statement / arguments / database / persistent,四字段表达"SQL + 参数 + 缓存标志"。
  2. query() 顶层函数(§9.3)—— 12 行代码构造空 Query;query_with / query_with_result 变体服务 query! 宏。
  3. bind 延迟错误(§9.4)——encode 失败存进 arguments 的 Err 分支,后续 bind 静默、fetch 时统一报。让链式风格不被 ? 打断。
  4. persistent 开关(§9.5)——where DB: HasStatementCache 守护;三家 in-tree 驱动都实现了它,挡住的是不实现该 marker 的第三方驱动。
  5. fetch/execute 薄包装(§9.6)——一行 executor.XXX(self).await,Query 本身不做 I/O。fetch_many / execute_many 被软弃用,生态方向走 raw_sql。
  6. QueryAs = Query + PhantomData<O>(§9.7)——通过 FromRow bound 自动映射 row 到 struct。
  7. QueryScalar = QueryAs<(O,)>(§9.8)——利用 tuple FromRow blanket 取第一列。
  8. Map = Query + Fn(Row)->O(§9.9)——闭包映射,FromRow 表达不了的条件逻辑用它。
  9. Execute trait 的四个 impl(§9.10)—— Query / QueryAs / QueryScalar / Map 各自能被 executor 消化。
  10. #[must_use] 攻击 forgotten .fetch 的编译期警告(§9.11)——类型级 API 防御。
  11. 四个函数的决策树(§9.12)—— 单值 scalar、多列 struct query_as、自定义 try_map、已 prepared query_statement。

下一章我们看 QueryBuilder<'q, DB>——动态拼 SQL 的类型安全工具,以及它如何用 push_bind 自动生成占位符(Postgres $N / MySQL 和 SQLite 的 ?)。当静态 SQL 字面量覆盖不了场景(动态 WHERE 条件、按列名投影、批量 VALUES 等),QueryBuilder 是下一步。它建立在本章的 Query 类型之上——.build() 方法返回 Query<'q, DB, A>,之后一切回到本章讲过的路径。所以本章理解透了,QueryBuilder 只是"多一层构造期的字符串操作"。

这条"逐步构建"的 API 分层思路贯穿整个 sqlx——Query 是最底层的执行单元,FromRow 在 Row 之上添加结构映射,QueryAs 在 Query 之上添加输出映射,QueryBuilder 在 Query 之前添加动态构造。每一层都建立在前一层之上、可以单独学习、组合使用——这就是 sqlx 作为"生态基础设施"能被各种上层库复用的原因。

9.15 Query 的五条实际观察

读完本章的类型链,有五条实战观察可以带走:

  1. Query 是零开销的类型组装器——构造时只做 struct 字段赋值,不做 I/O 或 SQL 解析。用户调 fetch 之前 Query 完全是静态数据。
  2. bind 的失败不阻止链式继续——失败会静默"毒化"后续的 bind(§9.4.2)。这让"链式风格"为代价换来"延迟错误"——生产用 ok 但调试时要注意定位。
  3. 持久 prepared statement 的成本在 server 侧——每个缓存的 statement 在数据库 server 占少量内存。长连接 + 高 SQL 基数可能让 server 的 statement cache 不下——第 22 章会讲怎么监控。
  4. fetch 返回 Stream——.await 消费——.fetch(pool) 返回的是 stream 不是 Future。把它错当 Future 直接 .await? 会编译错误。正确用法 while let Some(row) = stream.try_next().await? 或 .collect::<Vec<_>>().await。
  5. Query / QueryAs / QueryScalar / Map 是装饰模式而不是继承——四种类型通过包装关系层层叠加,而不是从一个基类派生。这是 Rust 里避免 trait-object 开销的标准做法。

这五条观察比"记住所有 API 签名"更有用——它们是 API 设计背后的工程思维。

9.16 类型链的组装可视化

本章涉及的所有类型和函数放一张图:

graph TD
    SQL["&amp;'q str SQL 字符串"]
    STMT["&amp;'q DB::Statement&lt;'q&gt;"]
    QFN["query(sql)"]
    QAFN["query_as::&lt;_, O&gt;(sql)"]
    QSFN["query_scalar::&lt;_, O&gt;(sql)"]
    QSTMT["query_statement(&amp;stmt)"]
    Q["Query&lt;'q, DB, A&gt;"]
    QA["QueryAs&lt;'q, DB, O, A&gt;"]
    QSC["QueryScalar&lt;'q, DB, O, A&gt;"]
    MAP["Map&lt;'q, DB, F, A&gt;"]
    BIND["Query.bind(x)"]
    TRYMAP["Query.try_map(|row| ...)"]
    FETCH["fetch_* / execute"]
    EXEC["Executor"]
    SQL --> QFN & QAFN & QSFN
    STMT --> QSTMT
    QFN --> Q
    QAFN --> QA
    QSFN --> QSC
    QSTMT --> Q
    Q --> BIND
    BIND --> Q
    Q --> TRYMAP
    TRYMAP --> MAP
    QA -.内嵌.-> Q
    QSC -.内嵌.-> QA
    MAP -.内嵌.-> Q
    Q --> FETCH
    QA --> FETCH
    QSC --> FETCH
    MAP --> FETCH
    FETCH --> EXEC

这张图显示了所有类型的流转关系:

  1. SQL 字符串(或已 prepared 的 Statement)通过四个顶层函数变成三种初始类型(query 和 query_statement 都产出 Query)。
  2. Query 可以 bind(自循环,返回同一个 Query)或 try_map(转成 Map)。
  3. QueryAs 内嵌 Query + PhantomData<O>;QueryScalar 内嵌 QueryAs<(O,)>;Map 内嵌 Query + 闭包。
  4. 所有四种最终 .fetch_*(&executor) 进入 Executor 的协议交互。

Query 是核心——其它三个都是它的包装。看这张图就能理解 sqlx 的查询 API 是围绕 Query 展开的一组薄装饰器——不是四个独立并行的类型。

9.17 流式 fetch 的生命周期陷阱

fetch() 返回 BoxStream<'e, Result<Row, Error>>——流式拉取 row。但流的生命周期约束有坑:

async fn list_users(pool: &PgPool) -> Vec<User> {
    let mut stream = sqlx::query_as::<_, User>("SELECT ...").fetch(pool);
    let mut users = Vec::new();
    while let Some(user) = stream.try_next().await.unwrap() {
        users.push(user);
    }
    users
}

看起来合理。但如果想"fetch 再并行处理每个 user":

async fn process_users(pool: &PgPool) -> Vec<Output> {
    let stream = sqlx::query_as::<_, User>("SELECT ...").fetch(pool);
    let tasks: Vec<_> = stream
        .map(|user_res| tokio::spawn(async move { process(user_res?).await }))
        .collect()  // 这一步可能出问题
        .await;
    // ...
}

问题出在 stream 本身:fetch(pool) 返回的 BoxStream<'e, _> 借用了 executor('e 来自 &pool),并且在整条流被消费完之前一直占着从池里取出的那条连接。逐条 spawn 本身能编译——每个 User 是 owned 值,可以 move 进任务;但这条流不是 'static,想把整条流 move 进 tokio::spawn 会报 E0597(实测)。更隐蔽的是连接占用:只要还在迭代它,那条连接就一直被这个查询占用,spawn 出去的任务如果又要用池,就可能和自己抢连接。

正确做法:

let users: Vec<User> = sqlx::query_as::<_, User>("...").fetch_all(pool).await?;
let tasks: Vec<_> = users.into_iter().map(|u| tokio::spawn(async move { process(u).await })).collect();

先 fetch_all 收集成 Vec——每个 User 变 owned(第 7 章 §7.21)——再并发 spawn。换来"fetch 过程不并发"但保证可移动性。

什么时候真用 fetch(流式)?——结果集大到不能全拉回内存(百万行日志分析)、或需要"每条立即处理"(实时数据管道)。常规业务 CRUD 几乎都用 fetch_all/fetch_one。

9.18 try_bind 和显式错误

sqlx-core/src/query.rs:103-110 有一个鲜为人知的方法:

pub fn try_bind<T: 'q + Encode<'q, DB> + Type<DB>>(
    &mut self,
    value: T,
) -> Result<(), BoxDynError> {
    let arguments = self.get_arguments()?;
    arguments.add(value)
}

try_bind 是 bind 的即时错误版——接 &mut self、返回 Result。如果 encode 失败,立即返回 Err 而不是延迟到 fetch。

用法:

let mut q = sqlx::query("UPDATE ...");
q.try_bind(some_value)?;  // 立即检测
q.try_bind(other_value)?;
q.execute(&pool).await?;

注意 try_bind 的错误类型是 BoxDynError,不是 sqlx::Error:函数若返回 sqlx::Result,裸写 ? 会报 E0277(实测),要写成 q.try_bind(v).map_err(sqlx::Error::Encode)?;只有函数的错误类型本身就是 Box<dyn Error + Send + Sync>(即 BoxDynError)时才能直接 ?——连 Box<dyn Error> 都不行(均已实测)。

相比 .bind().bind().execute() 的延迟错误,try_bind 给你精确的定位——哪一次 bind 失败立刻知道。代价是打破链式风格——代码更啰嗦。

try_bind 的典型用例是批量 bind 大量参数——每个参数成功率不 100%(比如用户输入),你希望在 bind 阶段就发现问题而不是 fetch 时:

for (k, v) in lots_of_params {
    q.try_bind(k)?;  // 哪个值失败立即拦下
    q.try_bind(v)?;
}

这是"延迟错误"设计的逃生舱——想要即时错误的场景还是有路。

9.19 设计判断题

Q1:为什么 bind 吃 self 返回 self,而不是 &mut self?

A:让 query(sql).bind(a).bind(b).fetch_one(pool) 这种流式 chained 写法成立。每次 bind 吃掉 self 再产出 self —— 链式的每一步都是独立的值。如果是 &mut self,用户得写 let mut q = query(...); q.bind(a); q.bind(b); q.fetch_one(pool) —— 打破链式感。这是 Rust builder pattern 的典型权衡。

Q2:为什么 QueryScalar<O> 不直接存 O 而是存 (O,)?

A:复用现有机制。(O,) 通过 tuple 的 FromRow blanket impl 自动实现 FromRow<Row> for (O,)——无需给 QueryScalar 单独写 FromRow 逻辑。如果 QueryScalar 直接存 O,就得给每个 O 类型加一套"单列从 row 取值"的特殊 FromRow impl——违反 DRY。

Q3:为什么 Query 的 persistent 字段默认 true?

A:业务默认假设是"同一 SQL 多次调用"。web handler 里的查询语句通常每个请求都执行、形状相同——缓存 prepared statement 省下每次的 Parse 开销。代价是一次性 SQL 多用了点 server 内存——但缓存有容量上限(Postgres 侧是 PgConnectOptions::statement_cache_capacity,默认 100,见 sqlx-postgres/src/options/mod.rs:88、345),满了会淘汰。所以默认 true 对绝大多数场景友好,特殊场景手动设 false。

9.20 常见用户代码模式对比

最后用几组对比展示不同 API 选择带来的代码感差异:

CRUD 单查 by id:

// 最常见 —— query_as + struct
let user: User = sqlx::query_as("SELECT * FROM users WHERE id = $1")
    .bind(user_id).fetch_one(&pool).await?;

同样能写但不推荐:

// query + manual try_get —— 啰嗦
let row = sqlx::query("SELECT id, name, email FROM users WHERE id = $1")
    .bind(user_id).fetch_one(&pool).await?;
let user = User {
    id: row.try_get("id")?,
    name: row.try_get("name")?,
    email: row.try_get("email")?,
};
// query + try_map —— 灵活但多一层闭包
let user = sqlx::query("SELECT id, name, email FROM users WHERE id = $1")
    .bind(user_id)
    .try_map(|row: PgRow| Ok(User {
        id: row.try_get("id")?,
        name: row.try_get("name")?,
        email: row.try_get("email")?,
    }))
    .fetch_one(&pool).await?;

第一种最简洁——业务代码默认用它。第二种完全手工——只在你故意不想依赖 FromRow 派生时(比如性能热点要按位置索引)。第三种适合"映射有动态逻辑"场景。

单值聚合查询:

// 优雅
let count: i64 = sqlx::query_scalar("SELECT COUNT(*) FROM users")
    .fetch_one(&pool).await?;

// 可以但丑
let (count,): (i64,) = sqlx::query_as("SELECT COUNT(*) FROM users")
    .fetch_one(&pool).await?;

// 最啰嗦
let row = sqlx::query("SELECT COUNT(*) FROM users").fetch_one(&pool).await?;
let count: i64 = row.try_get(0)?;

批量 UPDATE:

let affected = sqlx::query("UPDATE users SET active = false WHERE last_login < $1")
    .bind(OffsetDateTime::now_utc() - Duration::days(30))
    .execute(&pool)
    .await?
    .rows_affected();

注意用 execute 而不是 fetch_*——UPDATE 只要 rows_affected 数字、不关心哪几行。不带 RETURNING 的 UPDATE 一行都不返回,这时 fetch_one 拿到的是 Error::RowNotFound(fetch_one 归约到 fetch_optional,None 就转成这个错误,见 sqlx-core/src/executor.rs:115-129),不是列索引类的错误。

INSERT RETURNING:

// RETURNING 有返回行 —— 必须用 fetch
let new_user: User = sqlx::query_as(
    "INSERT INTO users (name, email) VALUES ($1, $2) RETURNING *"
).bind("Alice").bind("alice@example.com").fetch_one(&pool).await?;

这些模式覆盖业务 CRUD 的 80%。记住核心判断:

  • 有结果要消费 → fetch_*(one/all/optional)
  • 不关心结果、只要影响行数 → execute
  • 单列单值 → query_scalar
  • 多列映射 struct → query_as + FromRow
  • 动态逻辑 → query + try_map

9.21 query_statement:针对已 prepared 的快捷函数

sqlx-core 的第四个顶层函数 query_statement(sqlx-core/src/query.rs:500-512,另有一个带参数的 query_statement_with 在 515-529)少有人讲:

pub fn query_statement<'q, DB>(statement: &'q <DB as Database>::Statement<'q>) -> Query<'q, DB, ...>
where DB: Database,

它接受已经 prepared 的 Statement 而不是字符串。注意 sqlx 门面 crate 并没有重导出它(src/lib.rs:23-30 只导出 query / query_with / query_as / query_as_with / query_scalar / query_scalar_with / raw_sql),写 sqlx::query_statement 会报 E0425(实测)。用户侧的入口是 Statement trait 上声明的 query() / query_as() / query_scalar() 等方法(sqlx-core/src/statement.rs:62-89),各驱动用 impl_statement_query! 宏(sqlx-core/src/statement.rs:91-173)把它们展开成对 query_statement / query_statement_as / query_statement_scalar 的调用。

典型用法(示例代码):

use sqlx::{Executor, Statement};

let mut conn = pool.acquire().await?;
let stmt = conn.prepare("SELECT * FROM users WHERE id = $1").await?;
for id in user_ids {
    let user: User = stmt.query_as().bind(id).fetch_one(&mut *conn).await?;
    // ...
}

和 query(sql).persistent(true) 有什么区别?

  • query().persistent(true):每次用 SQL 字符串构造 Query——sqlx 内部按 SQL 文本查 statement cache,命中直接用已 prepared 版本。缓存 miss 时会一次 Parse 消息(外加一次 Describe)。
  • stmt.query():Query 里装的是 Statement 引用,但执行时照样按 SQL 文本查缓存。以 Postgres 为例,PgStatement 只存 SQL 和列/参数元数据、不存服务端语句 ID(sqlx-postgres/src/statement.rs:13-16),run 仍走 get_or_prepare;prepare() 当初已把语句放进缓存,所以通常命中。若它已被 LRU 淘汰或缓存容量设为 0,会重新 Parse,只是因为带着元数据可以省掉 Describe(sqlx-postgres/src/connection/executor.rs:57-78)。MySQL / SQLite 的执行路径只取 Statement 里的 SQL 文本(它们的 fetch_many 根本不读 statement())。

所以两者稳态性能基本相同(都避免重复 Parse),stmt.query() 并不能绕过缓存查找;它的实际价值是提前拿到参数与列的类型信息(Statement::parameters() / columns())。

实际使用里 query_statement 罕见——query() 默认 persistent(true) 就够(而且前者还要先 prepare、持有 stmt 借用)。语句缓存本身见第 12 章 §12.12 和第 16 章 §16.6。

9.22 sqlx 和 tokio-postgres 的 API 风格对比

把 sqlx 的 Query API 和 tokio-postgres 的接口放一起对照,有助于理解 sqlx 为什么这样设计。

tokio-postgres 的风格:

let client = /* ... */;
let rows = client.query(
    "SELECT id, name FROM users WHERE id = $1",
    &[&42i32]
).await?;

for row in rows {
    let id: i32 = row.get("id");   // 注意:panic on error
    let name: String = row.get("name");
    // ...
}

参数是 &[&(dyn ToSql + Sync)]——切片 + trait object + 借用。没有 builder、没有类型映射、没有 must_use。

sqlx 的风格:

let users: Vec<User> = sqlx::query_as("SELECT id, name FROM users WHERE id = $1")
    .bind(42)
    .fetch_all(&pool)
    .await?;

链式 builder、类型化的 bind、自动 FromRow 映射、must_use 防忘记执行。

两者的核心差异:

维度 tokio-postgres sqlx
参数绑定 &[&dyn ToSql] 一次传 .bind().bind() 链式
参数生命周期 借用直到 query 完成 Query 拥有参数 buffer(Args 里)
错误处理 Row 的 get 直接返回 T,失败 panic(另有 try_get) try_get 返回 Result
类型映射 手动从 Row 取每列 FromRow 派生 / query_as 宏
多 DB 支持 只 Postgres Postgres / MySQL / SQLite
代码量(业务侧) 更少(无 FromRow 声明) 多几行(FromRow 派生)但更类型安全

sqlx 的 builder 模式贵一点但换来:链式可读、强类型、跨 DB、must_use 防错——这些是业务代码视角的收益。tokio-postgres 更薄、更贴近协议——适合基础设施组件(比如自己写 connection pool、PgBouncer alternative)。

按上面这些差异,本专栏的建议是:业务后端用 sqlx、底层组件用 tokio-postgres。sqlx 的 Query API 设计就是朝着"业务代码友好"这条路优化——本章讲的每一条(链式 bind、延迟错误、must_use、四个顶层函数)都是这条主线的具体落点。

9.23 #[deprecated] 的 fetch_many:生态方向

sqlx-core/src/query.rs:195 和 224 两处 #[deprecated]:

#[deprecated = "Only the SQLite driver supports multiple statements in one prepared statement and that behavior is deprecated. Use `sqlx::raw_sql()` instead. See https://github.com/launchbadge/sqlx/issues/3108 for discussion."]
pub async fn execute_many ...

#[deprecated = "Only the SQLite driver supports multiple statements in one prepared statement and that behavior is deprecated. Use `sqlx::raw_sql()` instead. See https://github.com/launchbadge/sqlx/issues/3108 for discussion."]
pub fn fetch_many ...

(QueryAs / QueryScalar 的 fetch_many 用的是同一段文字;Map::fetch_many 那条少了末尾的 issue 链接,sqlx-core/src/query.rs:395。)

背景:prepared statement 原则上只装一条语句。Postgres / MySQL 一直就这样;只有 SQLite 驱动允许 query("UPDATE ...; SELECT ..."),因为 SQLite 的预处理接口是它执行 SQL 的唯一途径。0.7.4 的 release note 说得明白:其他数据库禁止一条 prepared statement 里放多条语句,本身就是一道额外的 SQL 注入防线(sqlx-0.8.6/CHANGELOG.md:573-583)。sqlx 决定把"多语句"路径从 Query API 撤出,挪到 raw_sql() 明确表达。

sqlx::raw_sql(sql_string) 是 0.7.4 的 [#3007] 加入的 API——专为"一次发多条语句、不做参数绑定、适合 migration/DDL 场景"——它明确不走 prepared statement,因此也不能带 bind 参数。

RawSql 的结构比 Query 还简单——只有一个字段 pub struct RawSql<'q>(&'q str);(sqlx-core/src/raw_sql.rs:18),也没有标 #[must_use]。它的 Execute impl 把"不预处理"写死了:statement() 返回 None、take_arguments() 返回 Ok(None)、persistent() 返回 false(sqlx-core/src/raw_sql.rs:121-137)。参数为 None 正是驱动走非预处理路径的开关(§9.5):Postgres 发简单查询 Query 消息、结果按文本格式解码,MySQL 发 COM_QUERY,SQLite 用临时语句、不进缓存(SQLite 上实测执行 raw_sql 前后 cached_statements_size() 不变)。它的 execute / execute_many / fetch / fetch_many / fetch_all / fetch_one 同样是对 Executor 同名方法的一行包装,且 fetch_many / execute_many 在这里不弃用。有一处 0.8.6 的瑕疵:RawSql::fetch_optional 实际调的是 executor.fetch_one(sqlx-core/src/raw_sql.rs:258-268),返回类型是 Row 而不是 Option<Row>,查不到时报 RowNotFound(SQLite 上实测);v0.9.0 才改成 executor.fetch_optional 并返回 Option(a3aa942a,#3924)。

所以遇到 fetch_many 的 deprecation warning 时的正确做法:如果你只是想"执行一条多语句脚本"——换成 sqlx::raw_sql(...).execute(&pool).await?;如果你真想"一条 prepared 里跑多条"——重新想想你为什么要这样,绝大多数场景都可以拆成多个 query。

9.24 Query API 的演进观察

sqlx 的 Query 层这些年怎么走到今天,sqlx-0.8.6/CHANGELOG.md 里有几个能查证的节点(行号按 0.8.6 那份 CHANGELOG;0.3.0 那条另用 git 标签核对):

  • 0.3.0(2020-03-29,sqlx-0.8.6/CHANGELOG.md:2193) —— 一次大幅重构:Query(query() 的返回值)被拿掉了 fetch_one / fetch_optional / fetch_all,用户必须先 .map(|row| ...) 得到 query::Map 才能 fetch。CHANGELOG 只写了 Row 带上连接生命周期、Query 提供 map();v0.3.0 标签下的 sqlx-core/src/query.rs 里,Query 上确实只剩 execute 和返回 Cursor 的 fetch,fetch_one / fetch_optional / fetch_all 都在 Map 上。也就是说,今天 Query 上那一排 fetch 方法,是后来又加回去的。
  • 0.3.3(2020-04-01,sqlx-0.8.6/CHANGELOG.md:2163) —— [#216] 把 Cursor、Query、QueryAs、query::Map、Transaction 统统标上 #[must_use],也就是 §9.11 那条防御。
  • 0.4.2(2020-12-19,sqlx-0.8.6/CHANGELOG.md:1851) —— [#852] 修掉 fetch_one / fetch_optional 可能吃满一个 CPU 核的问题。
  • 0.5.10(2021-12-29,sqlx-0.8.6/CHANGELOG.md:1545) —— [#1508] 把 .persistent(bool) 补给 QueryAs 和 QueryScalar(在此之前只有 Query 有)。
  • 0.7.4(2024-03-11,sqlx-0.8.6/CHANGELOG.md:573) —— [#3007] 加入 raw_sql API,同时把 execute_many() / fetch_many() 标记为 deprecated(§9.23)。这条 release note 把理由写得很清楚:一条 prepared statement 里放多条语句本来就只有 SQLite 支持,其他数据库把这条禁令当作抵御 SQL 注入的一道额外防线。
  • 0.8.0(2024-07-22,sqlx-0.8.6/CHANGELOG.md:389) —— [#2973] 的 GAT 改造(release note 原文:"Generic Associated Types in Database, replacing HasValueRef, HasArguments, HasStatement")让 Query<'q, DB, A> 里的 A 直接写成 DB::Arguments<'q>,不必再绕 HasArguments<'q>。
  • 0.8.6(2025-05-19,sqlx-0.8.6/CHANGELOG.md:18) —— [#3863] 在 Postgres 上让 persistent(false) 的查询走 unnamed statement,把这个开关的语义落实到协议层。

从 0.3.0 那次"把 fetch 方法全删掉"到今天"四个顶层函数 + 四种查询类型各司其职",中间隔了五年多。这不是一次设计出来的——fetch_optional 和 fetch_one 的错误语义分工、query_scalar 的存在、persistent 的下沉,每一条都是在后续版本里逐步补上的。

这条演进给 API 设计者的启示:早期用户要的就是"能跑";精细化的 API 分工(比如区分 fetch_one 和 fetch_optional 的错误语义)值得等到你真的知道用户会怎么用错,再动手。

9.25 Query API 的生产实战建议

读完本章 query API 的每一处细节,把生产实战观察收在这里:

规范 1:handler 内用 pool 不用 &mut conn

async fn handler(State(pool): State<PgPool>) -> Result<...> {
    // Good
    sqlx::query_as(...).fetch_one(&pool).await?
    // 避免
    let mut conn = pool.acquire().await?;
    sqlx::query_as(...).fetch_one(&mut *conn).await?  // PoolConnection 同样要写 &mut *conn
    // ...
    // 除非:同 handler 内多条相关 SQL 要同一连接(确保 tx / transaction-free 一致性)
}

规范 2:事务里用 &mut *tx

let mut tx = pool.begin().await?;
sqlx::query_as(...).fetch_one(&mut *tx).await?;  // 注意 DerefMut 的 * 必须
sqlx::query(...).execute(&mut *tx).await?;
tx.commit().await?;

规范 3:批量 UPDATE 用 execute

let affected = sqlx::query("UPDATE ... WHERE ...")
    .bind(...)
    .execute(&pool).await?
    .rows_affected();

不用 fetch_one 或 fetch_all——没有 RETURNING 的 UPDATE 没 row 可取。

规范 4:查询日志 + 错误带上 SQL 前缀

sqlx::query_as::<_, User>("SELECT ...").bind(id).fetch_one(&pool).await
    .map_err(|e| AppError::Database { query: "get_user", source: e })?

自定义 Error 包含"哪条查询失败"。sqlx 的 Error 只描述协议和解码层面的失败(RowNotFound / ColumnDecode / Database 等),业务级的"这是哪个查询"要靠你自己包装——否则日志里只有一句 RowNotFound,你不知道是哪一条。

规范 5:fetch_optional 不要滥用

看到 Option<User> 的 fetch_optional 方便,但如果你的业务语义是"必须有",用 fetch_one——让 RowNotFound 直接冒泡——比 fetch_optional?.ok_or(NotFound) 简洁且错误路径更精确。

规范 6:流式 fetch 只用于真正大结果集

fetch() 返回 stream,适合几万 / 几百万行的 ETL。一般 CRUD 都用 fetch_all(小结果集 Vec 比 stream 简单)。stream 要搭配正确的生命周期处理(§9.17 的陷阱)。

这六条是本专栏从前面各节归纳的建议——新手按这套走能避开大部分坑。第 22 章的生产实战会再加一套关于 Pool 层面的规范。

9.26 实战:完整 CRUD 模块示例

一个完整的 users 模块展示所有 Query API 的用法组合:

use sqlx::{PgPool, FromRow};
use time::OffsetDateTime;

#[derive(FromRow, Debug)]
pub struct User {
    pub id: i32,
    pub email: String,
    pub display_name: String,
    pub created_at: OffsetDateTime,
}

// ===== Create =====
// RETURNING *, 用 query_as + fetch_one
pub async fn create_user(
    pool: &PgPool,
    email: &str,
    display_name: &str,
) -> Result<User, sqlx::Error> {
    sqlx::query_as::<_, User>(
        r#"INSERT INTO users (email, display_name)
           VALUES ($1, $2)
           RETURNING id, email, display_name, created_at"#
    )
    .bind(email)
    .bind(display_name)
    .fetch_one(pool)
    .await
}

// ===== Read single =====
// 可能不存在,用 fetch_optional
pub async fn find_by_id(
    pool: &PgPool,
    id: i32,
) -> Result<Option<User>, sqlx::Error> {
    sqlx::query_as::<_, User>(
        "SELECT id, email, display_name, created_at FROM users WHERE id = $1"
    )
    .bind(id)
    .fetch_optional(pool)
    .await
}

// ===== Read list =====
pub async fn list_recent(
    pool: &PgPool,
    limit: i64,
) -> Result<Vec<User>, sqlx::Error> {
    sqlx::query_as::<_, User>(
        "SELECT id, email, display_name, created_at
         FROM users ORDER BY created_at DESC LIMIT $1"
    )
    .bind(limit)
    .fetch_all(pool)
    .await
}

// ===== Count =====
// 单值用 query_scalar
pub async fn count_active(pool: &PgPool) -> Result<i64, sqlx::Error> {
    sqlx::query_scalar("SELECT COUNT(*) FROM users WHERE active = true")
        .fetch_one(pool)
        .await
}

// ===== Update =====
// 不关心行内容,用 execute
pub async fn update_email(
    pool: &PgPool,
    id: i32,
    new_email: &str,
) -> Result<u64, sqlx::Error> {
    let result = sqlx::query("UPDATE users SET email = $1 WHERE id = $2")
        .bind(new_email)
        .bind(id)
        .execute(pool)
        .await?;
    Ok(result.rows_affected())
}

// ===== Delete =====
pub async fn delete_user(pool: &PgPool, id: i32) -> Result<u64, sqlx::Error> {
    let result = sqlx::query("DELETE FROM users WHERE id = $1")
        .bind(id)
        .execute(pool)
        .await?;
    Ok(result.rows_affected())
}

几条观察:

  1. CRUD 全部用 query() / query_as() / query_scalar() 三个函数——覆盖所有需求。
  2. 函数签名统一返回 Result<..., sqlx::Error>——把 sqlx 错误直接暴露给上层,由上层决定怎么包装(生产里通常会包成业务 Error)。
  3. Pool 作为 &PgPool 参数——不占用连接直到 fetch 触发,handler 代码可以共享 Pool(Axum 的 State 典型用法)。
  4. INSERT RETURNING 用 fetch_one、UPDATE/DELETE 用 execute——语义对应。

这个模块约 90 行 Rust 代码覆盖了 users 表的所有基本操作。和其他语言的 ORM(比如 Rails ActiveRecord)对比,行数相差不多,Rust 类型系统能保证的是 Rust 这一侧:bind(42) 的 42 按 i32 的 Type 编码成 INT4、fetch_one 的返回值是实现了 FromRow 的 User。但 query_as 函数不在编译期核对 SQL:列名拼错、列类型和字段对不上,都要到运行时才以 ColumnNotFound / ColumnDecode 之类的错误冒出来——想把这类错误挪到编译期,得用第 11 章的 query_as! 宏。

理解本章后你应该能做到:看一条业务需求"给用户 X 的订单数加 1",立即能写出 sqlx::query("UPDATE users SET order_count = order_count + 1 WHERE id = $1").bind(user_id).execute(&pool).await?; ——不用查文档。这种API 直觉来自理解四个顶层函数的分工,而不是死记方法签名。

9.27 'q 生命周期:一处易忽略的约束

回看本章所有方法签名,'q 生命周期出现在每个地方——但它的含义容易被忽略。

'q 是 Query 持有的"查询相关借用"的生命周期。具体包括:

  • SQL 字符串 &'q str(或 &'q Statement<'q>)——SQL 文本 / prepared statement 的借用。
  • Arguments 内部借用——例如 SQLite 的 SqliteArgumentValue::Text(Cow<'q, str>) 可能借 Rust 端字符串。

所有这些借用的生命周期都对齐到同一个 'q。用户代码里这个约束通常自然满足——因为 SQL 字符串往往是 &'static str(字面量),String 参数要么 move 要么 clone(bind 吃所有权或 clone)。

但在动态 SQL场景会踩坑。在一个 async 函数体里 let sql = format!(...) 然后立刻 query(&sql).fetch_all(pool).await 其实没问题——sql 活到函数结尾。真正卡住的是想把构造好的 Query 返回出去:

fn build_search(pattern: &str) -> Query<'_, Postgres, PgArguments> {
    let sql = format!("SELECT * FROM users WHERE email LIKE '%{pattern}%'");
    sqlx::query(&sql)
    // 编译错误 E0515:cannot return value referencing local variable `sql`
    // Query 只借用这个 &str,不拥有它;函数一返回 sql 就被 drop 了
    // (另:把 pattern 直接拼进 SQL 有注入风险,这里只演示生命周期)
}

同理,sqlx::query(&format!("...")) 里的 String 是临时值,会在所在语句结束时被 drop:写成一整条 sqlx::query(&format!(...)).fetch_all(pool).await? 没问题(临时值活到整条语句结束),但 let q = sqlx::query(&format!(...)); 再在下一行执行,就会报 E0716 temporary value dropped while borrowed(两种写法都实测过)。

两条解法:

  1. 让 SQL 字符串活得比 Query 长——把 String 提到调用方,query(&sql) 和 .await 都在它的作用域里。
  2. 用 QueryBuilder(第 10 章的主题)——QueryBuilder 自己拥有那个 String,.build() 出来的 Query 借的是 builder 内部的字符串,不依赖任何外部借用。

在动态 SQL 场景,QueryBuilder 通常是更省心的选择——除了生命周期,它还负责生成正确的占位符、避免把用户输入直接拼进 SQL。第 10 章会详细讲。

9.28 Query API 设计的通用启示

这章讲的 sqlx Query 层有几条可以迁移到其他项目的通用 API 设计原则:

1. 薄包装优于内置重逻辑。Query 的 fetch / execute 方法全是 executor.XXX(self).await ——Query 本身零 I/O 逻辑。这让 Query 的测试简单(不需要 mock executor,只测 struct 组装)、让 executor 的测试简单(不需要 mock Query,传 &str 也能 fetch)。两端都轻比"中间胖一层"好。

2. 装饰者优于继承。QueryAs / QueryScalar / Map 都是"Query + 一点额外"——不是"从 Query 派生"。Rust 没有传统继承,装饰者模式天然合适。装饰者的好处是每一层都能独立推导类型——用户把 QueryAs 传给一个要求 impl Execute 的地方,只需要看 QueryAs 自己的 Execute impl 就行。

3. 类型标记(PhantomData)降低运行时成本。Query<'q, DB, A> 的 DB 参数用 PhantomData 存储——零字节、零运行时开销,但让类型系统能感知 DB 身份。相比"传一个 Database trait object" 零开销。

4. 构造函数和结构体配对。query() 对 Query、query_as() 对 QueryAs、query_scalar() 对 QueryScalar——每个顶层函数对应一个具体结构体。用户记住一对"构造 + 类型"就行,不需要理解所有内部机制。

5. #[must_use] 是对用户疏忽的廉价防御。一个 attribute 换来编译期警告——防止"构造了 Query 但忘了 fetch"的常见 bug。这种防御几乎零成本(attribute 而不是运行时检查)、对新手极友好。

这五条原则在你自己的项目里设计 API 时都值得想想。sqlx 的 Query 层展示了"一个中等规模但设计良好的类型族"应该长什么样——若干装饰类型、薄方法、类型标记、构造函数、防御 attribute——每一部分都有明确职责,合起来让用户代码流畅。

第 10 章 QueryBuilder 讨论 sqlx 的动态 SQL 路径——怎么在运行时拼 WHERE id IN ($1, $2, $3) 这种占位符数量未知的查询——以及和第 11 章 query!() 宏的编译期校验形成对比。