SQLx 源码深度解析
第9章 Query 与 QueryAs:最小原语与查询生成链
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 等待完成。从类型系统角度看,每一步都是类型的精确转换:
query_scalar::<_, i64>("...")→QueryScalar<'_, DB, i64, DB::Arguments<'_>>。.bind(true)→ 同一个 QueryScalar,内部 Arguments 加了一个 bool。.fetch_one(&pool)→impl Future<Output = Result<i64, Error>>。.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?
- SQL 只执行一次——例如启动时跑迁移初始化。缓存这条 statement 没意义。
- SQL 基数巨大——动态生成的 SQL 有上千种模式。语句缓存是容量固定的 LRU(
sqlx-core/src/common/statement_cache.rs:1-14),满了就淘汰;模式太多只会不停淘汰重建(Postgres 每淘汰一条还要额外发一次Close),得不偿失。 - 中间件不兼容——
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),没有覆盖上面这些默认方法。
这种"薄包装"设计有两个价值:
- 为 Query 带来 "执行行为"——没有这些方法,用户得写
executor.fetch(my_query)而不是my_query.fetch(executor)。后者读起来更流畅("对这个查询 fetch" vs "用 executor fetch 这个查询")。 - 统一签名——所有 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)
}
}
两步:
executor.fetch_optional(self.inner)—— 拿回Option<DB::Row>。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::<_, T>(sql)"]
A -->|否| B{映射成 struct?}
B -->|是| QA["query_as::<_, Struct>(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 的核心类型链全部拆开:
- Query 结构四字段(§9.2)—— statement / arguments / database / persistent,四字段表达"SQL + 参数 + 缓存标志"。
- query() 顶层函数(§9.3)—— 12 行代码构造空 Query;query_with / query_with_result 变体服务
query!宏。 - bind 延迟错误(§9.4)——encode 失败存进 arguments 的 Err 分支,后续 bind 静默、fetch 时统一报。让链式风格不被
?打断。 - persistent 开关(§9.5)——
where DB: HasStatementCache守护;三家 in-tree 驱动都实现了它,挡住的是不实现该 marker 的第三方驱动。 - fetch/execute 薄包装(§9.6)——一行
executor.XXX(self).await,Query 本身不做 I/O。fetch_many / execute_many 被软弃用,生态方向走 raw_sql。 - QueryAs = Query +
PhantomData<O>(§9.7)——通过 FromRow bound 自动映射 row 到 struct。 - QueryScalar =
QueryAs<(O,)>(§9.8)——利用 tuple FromRow blanket 取第一列。 - Map = Query + Fn(Row)->O(§9.9)——闭包映射,FromRow 表达不了的条件逻辑用它。
- Execute trait 的四个 impl(§9.10)—— Query / QueryAs / QueryScalar / Map 各自能被 executor 消化。
- #[must_use] 攻击 forgotten .fetch 的编译期警告(§9.11)——类型级 API 防御。
- 四个函数的决策树(§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 的五条实际观察
读完本章的类型链,有五条实战观察可以带走:
- Query 是零开销的类型组装器——构造时只做 struct 字段赋值,不做 I/O 或 SQL 解析。用户调 fetch 之前 Query 完全是静态数据。
- bind 的失败不阻止链式继续——失败会静默"毒化"后续的 bind(§9.4.2)。这让"链式风格"为代价换来"延迟错误"——生产用 ok 但调试时要注意定位。
- 持久 prepared statement 的成本在 server 侧——每个缓存的 statement 在数据库 server 占少量内存。长连接 + 高 SQL 基数可能让 server 的 statement cache 不下——第 22 章会讲怎么监控。
- fetch 返回 Stream——.await 消费——
.fetch(pool)返回的是 stream 不是 Future。把它错当 Future 直接.await?会编译错误。正确用法while let Some(row) = stream.try_next().await?或.collect::<Vec<_>>().await。 - Query / QueryAs / QueryScalar / Map 是装饰模式而不是继承——四种类型通过包装关系层层叠加,而不是从一个基类派生。这是 Rust 里避免 trait-object 开销的标准做法。
这五条观察比"记住所有 API 签名"更有用——它们是 API 设计背后的工程思维。
9.16 类型链的组装可视化
本章涉及的所有类型和函数放一张图:
graph TD
SQL["&'q str SQL 字符串"]
STMT["&'q DB::Statement<'q>"]
QFN["query(sql)"]
QAFN["query_as::<_, O>(sql)"]
QSFN["query_scalar::<_, O>(sql)"]
QSTMT["query_statement(&stmt)"]
Q["Query<'q, DB, A>"]
QA["QueryAs<'q, DB, O, A>"]
QSC["QueryScalar<'q, DB, O, A>"]
MAP["Map<'q, DB, F, A>"]
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
这张图显示了所有类型的流转关系:
- SQL 字符串(或已 prepared 的 Statement)通过四个顶层函数变成三种初始类型(
query和query_statement都产出 Query)。 - Query 可以 bind(自循环,返回同一个 Query)或 try_map(转成 Map)。
- QueryAs 内嵌 Query +
PhantomData<O>;QueryScalar 内嵌QueryAs<(O,)>;Map 内嵌 Query + 闭包。 - 所有四种最终
.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_sqlAPI,同时把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())
}
几条观察:
- CRUD 全部用
query()/query_as()/query_scalar()三个函数——覆盖所有需求。 - 函数签名统一返回
Result<..., sqlx::Error>——把 sqlx 错误直接暴露给上层,由上层决定怎么包装(生产里通常会包成业务 Error)。 - Pool 作为
&PgPool参数——不占用连接直到 fetch 触发,handler 代码可以共享 Pool(Axum 的 State 典型用法)。 - 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(两种写法都实测过)。
两条解法:
- 让 SQL 字符串活得比 Query 长——把
String提到调用方,query(&sql)和.await都在它的作用域里。 - 用
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!() 宏的编译期校验形成对比。