Appearance
第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)+ execute(
query.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 等待完成。从类型系统角度看,每一步都是类型的精确转换:
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,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_with 和 query_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 失败了(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
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?
- SQL 只执行一次——例如启动时跑迁移初始化。缓存这条 statement 没意义。
- SQL 基数巨大——动态生成的 SQL 有上千种模式。缓存会撑爆 statement cache。
- 查询内存敏感——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。这种"薄包装"设计有两个价值:
- 为 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] 标记(query.rs:189, 225)——原因是只有 SQLite 支持一个 prepared statement 里多条语句,生态方向是让用户用 sqlx::raw_sql() 明确表达多语句。这是 0.8 的软弃用,0.9 可能会移除。
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: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)
}
}两步:
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、收集 / 返回。这条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 的选择:
- QueryAs:
O实现了 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::RowNotFoundfetch_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_one 和 fetch_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 的核心类型链全部拆开:
- 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守护,SQLite 下方法编译不存在。 - 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.13.1 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.14 类型链的组装可视化
本章涉及的所有类型和函数放一张图:
这张图显示了所有类型的流转关系:
- SQL 字符串通过四个顶层函数变成四种初始类型。
- Query 可以 bind(自循环,返回同一个 Query)或 try_map(转成 Map)。
- QueryAs 内嵌 Query +
PhantomData<O>;QueryScalar 内嵌QueryAs<(O,)>;Map 内嵌 Query + 闭包。 - 所有四种最终
.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_bind 是 bind 的即时错误版——接 &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_statement(query.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-postgres | sqlx |
|---|---|---|
| 参数绑定 | &[&dyn ToSql] 一次传 | .bind().bind() 链式 |
| 参数生命周期 | 借用直到 query 完成 | Query 拥有参数 buffer(Args 里) |
| 错误处理 | Row 的 get 直接返回 T,失败 panic | 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.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_one和fetch_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_one 或 fetch_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())
}几条观察:
- 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——语义对应。
这个模块约 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 生命周期出现在每个地方——但它的含义容易被忽略。
'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场景会踩坑:
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
}两条解法:
- 保持 sql 活到 await 之后——把
let _held = sql;放在 await 之后;大多数时候编译器会正确推导,但 async 的生命周期有时需要手动提示。 - 用
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!() 宏的编译期校验形成对比。