Skip to content

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

"An API is the combinator that glues a user's intent to the system's capability— the fewer glue layers you need, the better your primitives." —— Rust 生态 API 设计的一条常识

本章要点

  • Query<'q, DB, A>sqlx-core/src/query.rs:17-22)是 sqlx 用户 API 的核心类型——四个字段:statement(SQL 或 cached Statement)、arguments(参数集合 or 错误)、database(PhantomData)、persistent(是否缓存预处理)。
  • query() 顶层函数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 守护,SQLite 下这个方法编译不存在(第 3 章 §3.9 讨论过)。
  • 四个 fetch 方法(fetch / fetch_all / fetch_one / fetch_optional)+ executequery.rs:186-299)全部是对 Executor trait 的薄包装——executor.fetch(self) 而已。Query 本身不承担 I/O,只承担类型组装。
  • QueryAs<'q, DB, O, A>sqlx-core/src/query_as.rs:18-21)= 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:18-20)= QueryAs<'q, DB, (O,), A> 的包装——利用 (T,) 的 tuple FromRow blanket impl(第 8 章 §8.11)拿第一列值。
  • #[must_use = "query must be executed..."] 标在每个 Query 类型上——构造了但没执行直接丢弃会编译器警告,提前发现"忘了 .await"bug。

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

一条典型用户代码:

rust
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,executor 调用驱动层的 fetch_many 做协议交互。这条"Query 管组装,Executor 管执行"的分层是本章要建立的心智模型。

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

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

rust
/// 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

每次 execute 后是否缓存预处理语句。默认 true——sqlx 认为"同一条 SQL 大概率会被多次执行"。false 用于一次性 SQL,执行后立即关闭语句。

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

9.3 query() 顶层函数

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

rust
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 用字符串字面量、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_withquery_with_result

query.rs:670-692 有两个变体函数:

rust
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! 宏:宏展开后要传入 ImmutableArguments 容器(第 6 章 §6.8),不走 bind 链,直接用 query_with_result(sql, Ok(imm_args)) 构造。

9.4 .bind() 链:Arguments 消化

query.rs:75-103 的 bind 方法是全书最精巧的代码之一:

rust
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"的 Query 才有 "添加参数"的操作;其他形态的 A(比如 ImmutableArguments)是"已经固定"的。

9.4.2 bind 失败的延迟错误

get_arguments() 检查 self.arguments

rust
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 失败了(argumentsSome(Err(...))),get_arguments 返回错误。bind 方法收到错误不 propagate,只是 return self——继续返回原 Query。

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

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

方案 A:bind 立即 panic

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

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

方案 B:bind 返回 Result

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

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

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

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

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

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

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

9.5 persistent 开关

query.rs:123-141 只给 DB: HasStatementCache 实现:

rust
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 关闭缓存——单次执行后立即关 statement。

什么时候用 false?

  1. SQL 只执行一次——例如启动时跑迁移初始化。缓存这条 statement 没意义。
  2. SQL 基数巨大——动态生成的 SQL 有上千种模式。缓存会撑爆 statement cache。
  3. 查询内存敏感——prepared statement 在服务端留资源(Postgres 的 pg_stat_statement),长连接下多余的缓存会拖性能。

HasStatementCache 的实现方 Postgres / MySQL 两家支持缓存;SQLite 不支持——它不实现 HasStatementCache、所以 SQLite 下的 Query 没有 persistent 方法(编译期不存在)。

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

query.rs:143-299 大片内容是 fetch / execute 方法族——全部只做一件事:executor.XXX(self)

rust
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。这种"薄包装"设计有两个价值:

  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] 标记(query.rs:189, 225)——原因是只有 SQLite 支持一个 prepared statement 里多条语句,生态方向是让用户用 sqlx::raw_sql() 明确表达多语句。这是 0.8 的软弃用,0.9 可能会移除。

9.6.1 execute 返回 QueryResult,其它返回 Row

executefetch_* 的本质差异

  • 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:18-21

rust
/// 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 顶层函数(query_as.rs 里另一个位置):

rust
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 方法

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

rust
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、收集 / 返回。这条row → O 的映射是 QueryAs 相对 Query 的核心附加功能。

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

query.rs:154-187 还有一组 map / try_map 方法返回 Map<'q, DB, F, A>

rust
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 的选择:

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

query_as().bind().fetch_one vs query().bind().try_map(|row| { ... }).fetch_one——前者简洁,后者灵活。实际生产代码里 QueryAs 占 90%。

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

sqlx-core/src/query_scalar.rs:18-20

rust
#[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 顶层函数:

rust
pub fn query_scalar<'q, DB, O>(sql: &'q str) -> QueryScalar<'q, DB, O, DB::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>:闭包映射

query.rs:36-40

rust
#[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 和 QueryAs 几乎等价——都是"Query + 输出转换"。差异是:

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

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

rust
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),
            _ => Err(Error::RowNotFound),
        }
    })
    .fetch_all(&pool).await?

FromRow 表达不了这种条件逻辑——只能 try_map。

9.10 Execute trait 的四个 impl

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

query.rs:41-73 的 Query 实现:

rust
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 / Map 的 Execute impl 都是 delegate 到内嵌 Query 的对应方法——没有独立逻辑。四个 impl 共同构成"可执行查询"的完整类型集合。

9.11 #[must_use] 的防御

四个 Query 类型上都有:

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

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

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

警告信息是"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),收益是 pct% 的 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)Query<'_, DB, ...>已 prepared 的 Statement 反复执行

决策树:

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

9.12.1 .fetch_one / .fetch_optional 的边界行为

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

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

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

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

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

两者对"查到多行但只要一行"的处理相同——都只取第一行、后续行被 driver 读掉后丢弃。但源码(query_as.rs:170-175)警告:最好保证查询本身只返回一行——加 LIMIT 1 或用 PRIMARY KEY 过滤——让数据库可以用更优的查询计划。

fetch_onefetch_optional 的 API 选择取决于业务语义:

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

混用会产生笨重的代码:

rust
// 不推荐: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 仍然冒泡":

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

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

9.13 本章小结

本章把 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 守护,SQLite 下方法编译不存在。
  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.13.1 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.14 类型链的组装可视化

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

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

  1. SQL 字符串通过四个顶层函数变成四种初始类型。
  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.15 流式 fetch 的生命周期陷阱

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

rust
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":

rust
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;
    // ...
}

如果 process 有 await、每个 user 的处理耗时不一,tokio::spawn 需要 'static + Send——但 stream 借用 pool,stream 里的 user 借用了 conn buffer(Row 是 'static 但 stream 的元素是 Result<Row, Error>——这个 Future 可能借用外部)。实际这种写法通常报生命周期错。

正确做法

rust
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.17.1)——再并发 spawn。换来"fetch 过程不并发"但保证可移动性。

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

9.16 try_bind 和显式错误

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

rust
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_bindbind 的即时错误版——接 &mut self、返回 Result。如果 encode 失败,立即返回 Err 而不是延迟到 fetch。

用法:

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

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

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

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

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

9.17 设计判断题

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 内存——但缓存有容量上限(PoolOptions::max_statement_cache_capacity 可以调),满了会 LRU 淘汰。所以默认 true 对 95% 场景友好,5% 特殊场景手动设 false。

9.17.1 常见用户代码模式对比

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

CRUD 单查 by id

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

同样能写但不推荐:

rust
// 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")?,
};
rust
// 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 派生时(比如性能热点要按位置索引)。第三种适合"映射有动态逻辑"场景。

单值聚合查询

rust
// 优雅
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

rust
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 数字、不关心哪几行。用 fetch_one 会 ColumnIndexOutOfBounds(UPDATE 没 RETURNING 不返回行)。

INSERT RETURNING

rust
// 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.17.2 query_as vs query_as!:再次强调区别

第 8 章 §8.16.1 已经讲过两条并行路径,这里从 API 层面再强调一次——因为 query_as 函数和 query_as! 宏的名字太像,新手经常混淆。

rust
// query_as 函数:运行时 FromRow 映射
#[derive(FromRow)]
struct User { id: i32, name: String }
let u: User = sqlx::query_as::<_, User>("SELECT id, name FROM users WHERE id = $1")
    .bind(42).fetch_one(&pool).await?;

// query_as! 宏:编译期 describe 生成代码
struct User { id: i32, name: String }  // 无 derive
let u = sqlx::query_as!(User, "SELECT id, name FROM users WHERE id = $1", 42)
    .fetch_one(&pool).await?;

两者的 API 表面近似但实现路径完全独立

  • query_as::<User>(sql) 返回 QueryAs<'_, DB, User, Arguments>——本章的主题。
  • query_as!(User, sql, params) 是 proc macro 宏——展开生成代码(第 1 章 §1.5.1 讨论过),内部用 query_with_result + 手写 try_map + try_get_unchecked——根本不生成 QueryAs 类型。

两者的 .fetch_one(&pool).await? 表面相同——但生成的 Future 类型不同。一旦理解这条"同名不同实现"的关系,你看 sqlx 代码时就不会把这两者搞混。

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

sqlx 的第四个顶层函数 query_statementquery.rs:500-529)少有人讲:

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

它接受已经 prepared 的 Statement 而不是字符串。典型用法:

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

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

  • query().persistent(true):每次用 SQL 字符串构造 Query——sqlx 内部查 statement cache,命中直接用已 prepared 版本。缓存 miss 时会一次 Parse 消息
  • query_statement(&stmt):用已经 prepared 的 Statement 对象——肯定不会再 Parse——跳过 cache 查询。

两者稳态性能相同(都避免重复 Parse),query_statement 的优势是 bypass cache lookup 的细微开销——热点代码微优化。

实际使用里 query_statement 罕见——query().persistent(true) 的组合就够(而且前者还要先 prepare、持有 stmt 借用)。本书第 12 章讲 Connection::prepare 时会再补充。

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

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

tokio-postgres 的风格

rust
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 的风格

rust
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-postgressqlx
参数绑定&[&dyn ToSql] 一次传.bind().bind() 链式
参数生命周期借用直到 query 完成Query 拥有参数 buffer(Args 里)
错误处理Row 的 get 直接返回 T,失败 panictry_get 返回 Result
类型映射手动从 Row 取每列FromRow 派生 / query_as 宏
多 DB 支持只 PostgresPostgres / 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.17.4 #[deprecated] 的 fetch_many:生态方向

query.rs:186-189, 225-228 两处 #[deprecated]

rust
#[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."]
pub fn fetch_many ...

背景:prepared statement 原则上只装一条语句。Postgres / MySQL 一直就这样;SQLite 历史上允许 prepare("UPDATE ...; SELECT ...") 但使这件事跨 DB 可用需要大量 hack。sqlx 决定把"多语句"路径从 Query API 撤出,挪到 raw_sql() 明确表达。

sqlx::raw_sql(sql_string) 是 0.8 加入的新 API——专为"一次发多条语句、不做参数绑定、适合 migration/DDL 场景"——走 simple query 协议而非 extended query。

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

9.17.5 Query API 的演进观察

sqlx 的 Query 层从 0.3 到 0.8 的演进值得记住——理解今天的形态怎么来的:

  • 0.3 —— 最初 Query 只有 execute / fetch_all 两个方法;没有 stream fetch、没有 fetch_optional 的区分。
  • 0.4 —— 加入 fetch 流式;区分 fetch_onefetch_optional(0.3 的 fetch_one 对 0 行返回 Option::None,0.4 改成 Error::RowNotFound)。
  • 0.5 —— QueryAs 独立成类型(之前是 Query::map 的特化);引入 query_scalar。
  • 0.6 —— persistent 方法加入;#[must_use] attribute 上架。
  • 0.7 —— GAT 之后 lifetime 约束大幅简化;Query/QueryAs/QueryScalar 的签名从 HasArguments<'q> HRTB 解脱出来。
  • 0.8 —— fetch_many / execute_many#[deprecated]raw_sql() 作为替代路径加入。

每次改动都朝"更精细的 API + 更清晰的语义"方向。对比 2019 年的 0.3 和 2026 年的 0.8,顶层 API 从"两个方法粗放执行"变成"四个函数 + 四种类型精细分工"——这不是过度设计,是从五年生产使用反馈里长出的结构。

这条演进给 API 设计者的启示一开始做粗、用几年再精细化。sqlx 没试图在 0.3 就把 fetch_optional / query_scalar / Map 全设计出来——早期用户需要的就是"能跑";精细化的 API(例如区分 fetch_one vs fetch_optional 的错误语义)是从实际问题里反推出的。

9.17.6 Query API 的生产实战建议

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

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

rust
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?
    // ...
    // 除非:同 handler 内多条相关 SQL 要同一连接(确保 tx / transaction-free 一致性)
}

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

rust
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

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

不用 fetch_onefetch_all——没有 RETURNING 的 UPDATE 没 row 可取。

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

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

自定义 Error 包含"哪条查询失败"——生产排查时节省至少 30 分钟。sqlx 的 Error 只含协议级细节,业务级的"这是哪个查询"靠你自己包装。

规范 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.15 的陷阱)。

这六条规范是 sqlx 用户社区的经验浓缩——新手按这套走能避开 90% 的坑。第 22 章的生产实战会再加一套关于 Pool 层面的规范。

9.17.7 实战:完整 CRUD 模块示例

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

rust
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_oneUPDATE/DELETE 用 execute——语义对应。

这个模块约 70 行 Rust 代码覆盖了 users 表的所有基本操作。和其他语言的 ORM(比如 Rails ActiveRecord)对比,行数相差不多但每一行都是类型安全的——Rust 编译器会保证 bind(42) 的 42 被 encode 成 INT4、fetch_one 的返回是类型匹配的 User struct。运行时错误主要是"连接失败 / 约束违反 / 返回 0 行"这些业务级的、不是"类型映射错了"的低级错误。

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

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

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

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

  • 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场景会踩坑:

rust
async fn search(pool: &PgPool, pattern: &str) -> Result<Vec<User>, Error> {
    let sql = format!("SELECT * FROM ...");  // 函数内局部 String
    sqlx::query_as::<_, User>(&sql).fetch_all(pool).await
    // 编译错误:sql 在 await 之前 drop
}

两条解法

  1. 保持 sql 活到 await 之后——把 let _held = sql; 放在 await 之后;大多数时候编译器会正确推导,但 async 的生命周期有时需要手动提示。
  2. QueryBuilder(第 10 章的主题)——QueryBuilder 内部把 SQL 字符串作为 String 拥有,不依赖外部借用。

在动态 SQL 场景,QueryBuilder 几乎是唯一正确选择。普通 query() 搭配 format! 的路径生命周期过于脆弱——第 10 章会详细讲。

9.17.9 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!() 宏的编译期校验形成对比。

基于 VitePress 构建