Appearance
第3章 Database trait 家族:用 GAT 收束异构驱动
"A trait is a contract between the implementor and the user— and a good contract says exactly what's needed, no more, no less." —— 任何一个 trait 的第一次 code review
本章要点
Databasetrait(sqlx-core/src/database.rs:72)是整个 sqlx 类型体系的收束点——把驱动特有的 11 种类型(7 个普通关联类型 + 4 个 GAT)全部声明为关联类型,再加两个 const(NAME/URL_SCHEMES),让上层 API 只需要DB: Database一个泛型参数。- 其中四个是泛型关联类型(GAT):
type ValueRef<'r>、type Arguments<'q>、type ArgumentBuffer<'q>、type Statement<'q>——它们带生命周期参数。GAT 在 Rust 1.65(2022)才稳定,sqlx 0.7 是第一个利用 GAT 的版本。 - 没有 GAT 的 0.6 时代,sqlx 用
HasValueRef<'r>这样的"带生命周期的 helper trait"绕过——这种 workaround 让 trait 边界变得极其冗长,每个where子句都要重复写三到五个约束。 NAME和URL_SCHEMES两个关联常量让 Any 驱动能在运行时按 URL scheme 分发(见第 2 章)——这不是显眼的特性,但决定了"同一个 AnyConnection 能连所有 DB"这件事能不能做到。HasStatementCache是一个无方法的 marker trait——Postgres / MySQL 实现它,SQLite 不实现。上层 API(Query::persistent、Connection::cached_statements_size)通过where DB: HasStatementCache在类型系统层面把"语句缓存"这个能力锁进类型。- 对照 Postgres / MySQL / SQLite 三家的
impl Database,你会发现每一行的具体类型都不同但结构完全一样——这就是 trait 家族设计要达成的"等价替换"形态。
3.1 问题引入:一个 DB 要扛 11 个关联类型
上一章画过这行代码:
rust
let user: User = sqlx::query_as!(User, "SELECT id, name FROM users WHERE id = $1", id)
.fetch_one(&pool)
.await?;以及它展开后生成的那段匿名 Future:
rust
::sqlx::__query_with_result::<::sqlx::Postgres, _>(...)
.try_map(|row: ::sqlx::postgres::PgRow| { ... })注意第二行里的两个类型——Postgres 和 PgRow。如果你把 Postgres 换成 MySql,那第二行的 PgRow 必须同步换成 MySqlRow——这是 sqlx-core 的 API 在声明时就约束好的。sqlx::query_as 不知道具体是哪家数据库、不知道具体的 Row 类型长什么样——它只知道"你给我一个 DB: Database,我从 DB::Row 里解码"。
问题来了:这个"只知道 DB"的设计要求 Row、Column、Value、Arguments 等所有具体类型都能从 DB 这一个泛型参数推出来。Rust 里唯一能做到这件事的机制是关联类型(associated type):
rust
pub trait Database {
type Row;
type Column;
type Value;
// ...
}
fn fetch_one<DB: Database>(...) -> <DB as Database>::Row { ... }但 sqlx 的 11 个关联类型里有四个带生命周期参数:
rust
type ValueRef<'r>: ValueRef<'r, Database = Self>;
type Arguments<'q>: Arguments<'q, Database = Self>;
type ArgumentBuffer<'q>;
type Statement<'q>: Statement<'q, Database = Self>;type ValueRef<'r> 不是普通关联类型——它是泛型关联类型(GAT)。GAT 在 Rust 1.65(2022 年 11 月)才稳定,sqlx 0.7(2023 年 7 月)是第一个大版本全面用 GAT 的。0.6 及之前的 sqlx 用了一套非常笨拙的绕法——本章第 3.3 节会详细看那段历史。
这一章的任务,就是把 Database trait 的每一个关联类型、每一个 trait 边界拆开,解释它们为什么必须这样写、GAT 消除了哪些痛苦、HasStatementCache 这种"无方法标记 trait"怎么当作类型系统里的能力开关。读完本章,你对 DB: Database 这个约束在上层 API 的每一处出现都能立刻在脑子里展开成完整的关联类型集合。
3.2 Database trait 的全貌
sqlx-core/src/database.rs:72-113 的原文(本章后续讨论都以此为锚):
rust
pub trait Database: 'static + Sized + Send + Debug {
type Connection: Connection<Database = Self>;
type TransactionManager: TransactionManager<Database = Self>;
type Row: Row<Database = Self>;
type QueryResult: 'static + Sized + Send + Sync + Default + Extend<Self::QueryResult>;
type Column: Column<Database = Self>;
type TypeInfo: TypeInfo;
type Value: Value<Database = Self> + 'static;
type ValueRef<'r>: ValueRef<'r, Database = Self>;
type Arguments<'q>: Arguments<'q, Database = Self>;
type ArgumentBuffer<'q>;
type Statement<'q>: Statement<'q, Database = Self>;
const NAME: &'static str;
const URL_SCHEMES: &'static [&'static str];
}一共十一个关联类型 + 两个关联常量 + 四条 super-trait bound('static + Sized + Send + Debug)。把这十一个类型按功能分成四组:
后文的 3.4–3.7 节按这四组依次展开。
先看几条 super-trait bound:
'static:Database实现者(Postgres / MySql / Sqlite)都是零大小 unit struct——pub struct Postgres;——所以没有非 'static 引用。但'static不是自动推出来的,它是给下游 API 用的:"凡是DB: Database,我就能把DB放进Arc里、跨线程传递、跑在tokio::spawn里"。没有它,上层所有where 'static的泛型边界都会断掉。Sized:意味着DB不能是dyn Database形式——这和 Any 驱动形成对比:Any 用Box<dyn AnyConnectionBackend>绕开Sized约束,但它不是Database而是自己的Anyunit type 实现Database。Send:必须能跨线程——因为Pool<DB>的PoolInner会被多个 tokio task 共享。Debug:为了让错误信息、tracing span 能打印出驱动名字。
这四条 bound 加起来把 Database 实现者钉死为"一个零开销的类型 token"——它不持有任何状态,只是类型系统里的一个标识,告诉 Executor::fetch 等泛型方法应该去找哪一套具体的 Row / Column / Arguments 实现。
3.2.1 对照 diesel 的 Backend trait
Rust 生态里做"驱动抽象"的另一条路线是 diesel。把 diesel 2.x 的 Backend trait 和 sqlx 的 Database 放在一起比,能看出两种哲学的差异:
rust
// diesel 2.x 的 Backend trait(简化)
pub trait Backend: Sized + Send {
type QueryBuilder: QueryBuilder<Self>;
type RawValue<'a>;
type BindCollector<'a>: BindCollector<'a, Self>;
}
// 和
pub trait SqlDialect: Backend {
type ReturningClause;
type OnConflictClause;
type InsertWithDefaultKeyword;
type BatchInsertSupport;
// ...
}几条对比:
- diesel 的关联类型数量相当(
QueryBuilder/RawValue/BindCollector+ SqlDialect 里七八个),但语义完全不同——diesel 的关联类型描述"方言特性"(是否支持 RETURNING、ON CONFLICT),sqlx 的关联类型描述"数据形态"(Row、Column、Value)。 - diesel 没有 sqlx 的
Row/Column——因为 diesel 的结果映射是通过FromSqlRow派生宏按 DSL 表达式静态展开的,不需要运行时的 Row 抽象。 - diesel 有 sqlx 没有的
QueryBuilder——这是 diesel DSL 的 SQL 字符串生成器。sqlx 不需要,因为用户写的 SQL 就是原始字符串,不经过构造器翻译。
两种设计的本质差异可以浓缩成一句话:diesel 的 Backend trait 描述的是"如何从 Rust 表达式生成 SQL",sqlx 的 Database trait 描述的是"如何把 SQL 字节流和连接池接入 Rust 的泛型系统"。前者是"出"的问题,后者是"入"的问题——两种工具包的方向完全不同。
这个对照也解释了为什么 sqlx 的 Any 驱动能存在而 diesel 没有等价物——sqlx 抽象的"数据形态"在三家 DB 下大致等价,运行时擦除一层并不太失真;而 diesel 抽象的"方言特性"在三家 DB 下差异巨大(Postgres 的 RETURNING、SQLite 的 RETURNING-via-triggers、MySQL 的 last_insert_id),运行时无法擦除得干净。
3.3 为什么是 GAT:没有泛型关联类型之前的痛苦
要解释 GAT 的必要性,先看没有 GAT 时 sqlx 0.6 是怎么写的。
0.6 版本(以 sqlx-core 0.6.3 为代表)的 Database trait 大致长这样:
rust
// 0.6 时代的简化版
pub trait Database:
for<'r> HasValueRef<'r, Database = Self>
+ for<'q> HasArguments<'q, Database = Self>
+ for<'q> HasStatement<'q, Database = Self>
{
type Connection: Connection<Database = Self>;
type Row: Row<Database = Self>;
// ...
}
pub trait HasValueRef<'r> {
type Database: Database;
type ValueRef: ValueRef<'r, Database = Self::Database>;
}
pub trait HasArguments<'q> {
type Database: Database;
type Arguments: Arguments<'q, Database = Self::Database>;
type ArgumentBuffer;
}
pub trait HasStatement<'q> {
type Database: Database;
type Statement: Statement<'q, Database = Self::Database>;
}注意这三个 "Has*" trait:每个都带一个生命周期参数,把原本应该是 GAT 的类型变成"HKT 仿真"——通过 for<'r> 全称量化 bound 给 Database trait 附加约束。
这种写法的代价是,上层 API 每次用 DB::Arguments<'q> 都要写一长串 bound。比如 0.6 版的 Query::execute 要这样声明:
rust
pub fn execute<'e, 'q, E, A>(
self,
executor: E,
) -> impl Future<Output = Result<DB::QueryResult, Error>> + Send + 'e
where
'q: 'e,
E: Executor<'e, Database = DB>,
DB: Database + for<'r> HasValueRef<'r, Database = DB>,
A: 'q + IntoArguments<'q, DB>,
DB: for<'q2> HasArguments<'q2, Database = DB>,
// ... 还要再写两三条每次用到 DB::Arguments<'q> 都要追加一条 DB: HasArguments<'q2, ...>——因为编译器不会自动帮你"把 type Arguments 这个从属投影出来"(这就是 higher-kinded type 不直接支持的核心痛点)。社区把这种模式戏称为"HRTB soup"(for<'r> ... bound 的一碗汤)。
GAT 稳定之后,这一切变成:
rust
pub trait Database: 'static + Sized + Send + Debug {
type ValueRef<'r>: ValueRef<'r, Database = Self>;
type Arguments<'q>: Arguments<'q, Database = Self>;
// ...
}上层 API 可以直接写 fn execute<'q, DB: Database>(args: DB::Arguments<'q>)——编译器自动投影,不再需要 HasArguments 这种中间层。sqlx-core 0.7 的 Query 类型(query.rs:72-96)能够写得远比 0.6 干净:
rust
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,
}&'q DB::Statement<'q> 这个类型在 0.6 时代需要约 5 行 where 才能表达,GAT 之后直接写在字段位置——这是 GAT 对 sqlx 的最大价值。
从 0.6 升到 0.7 的 migration 指南里有一段直接说明了动因(见 sqlx 0.7 的 CHANGELOG):
The internal type system has been rewritten to use generic associated types (GATs), eliminating the need for the
HasValueRef/HasArguments/HasStatementtrait soup. Downstream drivers and users of customDatabaseimpls will need Rust 1.65+.
这是 sqlx 最低 Rust 版本从 1.60 跳到 1.65 的直接原因——它吃了一个 MSRV bump 来换 trait 家族的清爽。这条 trade-off 放在 2023 年的 Rust 生态里相当合理,因为那时候主流项目已经普遍在 1.70+ 了。
3.3.1 GAT 带来的新代价
GAT 不是免费午餐。sqlx 的实际使用里有两类新问题:
问题 1:lifetime inference 失败。GAT 下 DB::Arguments<'q> 的 'q 必须由编译器从上下文推导。在一些复杂的异步调用链里,编译器会推不出来,报出 "implementation is not general enough" 之类的错误。sqlx 0.7 的 Issue tracker 里有若干这类帖子——用户需要手动标注 'q,或者用 type alias 把 GAT 投影固化。
问题 2:Object safety 丢失。带 GAT 的 trait 是 non-object-safe 的——你不能 dyn Database。这正是为什么 Any 驱动需要自己的 AnyConnectionBackend trait(所有方法用 BoxFuture 返回,无 GAT)而不能直接 dyn Database。第 2.6 节讨论的"运行时多态叛徒"在类型系统层面的另一种解释就是:GAT + object-safety 天然不兼容,Any 必须另起炉灶。
这两个代价对 sqlx 的影响都是可控的——第一个问题靠文档和错误信息缓解,第二个问题通过 Any 驱动的独立 trait 绕开。GAT 总体是净收益。
3.3.2 如何读懂 sqlx 的 trait 边界编译错
GAT + trait 家族的一个直接副作用:编译错信息会变长。新手常常遇到这种错:
error[E0277]: the trait bound `&mut Transaction<'_, Postgres>: Executor<'_>`
is not satisfied
--> src/main.rs:42:5
|
42 | sqlx::query!("SELECT ...").fetch_one(&mut tx).await?;
| ^^^^^^^^^ the trait `Executor<'_>`
| is not implemented for
| `&mut Transaction<'_, Postgres>`
|
= help: the following other types implement trait `Executor<'c>`:
&'c Pool<DB>
&'c mut <DB as Database>::Connection这条错误在第 1 章"让步二·附"提到过——0.8 之后 &mut Transaction 不再自动是 Executor。但错误信息里藏着第二条线索:help 里列出的候选实现是用关联类型 <DB as Database>::Connection 描述的。读懂它需要先读懂 Database trait 的关联类型投影语法。
三个常见 sqlx 错误信息模式:
| 错误模式 | 含义 | 解决 |
|---|---|---|
trait bound <X as Database>::Row: Row not met | 某个 Row 关联类型的 super-trait bound 缺失 | 一般是 impl 顺序问题 |
implementation is not general enough | GAT 生命周期推断失败 | 手动标注 'q |
<X as Database>::ValueRef<'_> doesn't implement ... | ValueRef 的 GAT 投影缺少某个 trait 实现 | 检查 Decode<DB> 的覆盖 |
读 sqlx 错误信息的第一步永远是把 <X as Database>::Y 翻译成具体类型——<Postgres as Database>::Row 就是 PgRow、<Postgres as Database>::ValueRef<'_> 就是 PgValueRef<'_>。一旦翻译过来,问题就从抽象 trait bound 变成具体类型缺 impl,修起来容易得多。
3.4 读取链:Row / Column / Value / ValueRef / QueryResult
这是 Database trait 里最大的一组关联类型——五个。它们共同描述"从数据库读回一行,怎么拆开、怎么解码"。
3.4.1 Row:一行数据的抽象
sqlx-core/src/row.rs:14-16 的 Row trait:
rust
pub trait Row: Unpin + Send + Sync + 'static {
type Database: Database<Row = Self>;
// ...
}Unpin + Send + Sync + 'static 四条约束——这比 Database 本身还严格。每一条都有直接用途:
Unpin:允许 Row 在异步流里自由移动(BoxStream<Row>的要求)。Send + Sync:Row 经常在 tokio task 之间流转。'static:Row 不能借用外部数据。注意这和ValueRef<'r>形成对比——Row 持有值,ValueRef 持有引用。
Row::Database = Self:这是自反关联类型约束。它说"我这个 Row 的 Database 关联类型,指回去得是能产生我的那个 Database"。展开成具体:PgRow::Database = Postgres,而 Postgres::Row = PgRow——两者形成闭环。这条约束让编译器能在 fn take<R: Row>(r: R) -> R::Database::Row 这种泛型代码里做等价替换。
Row trait 的方法包括 try_get<T>(index) / try_get_raw(index) / columns() / try_column(index) / len()——全部都返回泛型或关联类型的值。具体实现由驱动自己给出:PgRow 持有 Postgres 线路协议解析后的 DataRow 字节,MySqlRow 持有 MySQL 的字段值数组,SqliteRow 持有 SQLite statement handle 的 column() 指针快照。三家的存储格式完全不同,但都满足 Row trait。
3.4.2 Column:列元数据
sqlx-core/src/column.rs:6-22:
rust
pub trait Column: 'static + Send + Sync + Debug {
type Database: Database<Column = Self>;
fn ordinal(&self) -> usize;
fn name(&self) -> &str;
fn type_info(&self) -> &<Self::Database as Database>::TypeInfo;
}Column 是列元数据——它不持有数据值,只持有"这一列叫什么、类型是什么、顺序是多少"。和 Row 的关系是:"每一 Row 里有一组 Column"——Row::columns() 返回 &[DB::Column]。
一个很重要的观察:Column 的 type_info 返回的是 &DB::TypeInfo 而不是 DB::TypeInfo 的克隆。这是因为列类型信息在一个预处理语句生命周期内是不变的(PostgreSQL 的 RowDescription 在 Parse 阶段就被服务端返回了),后续每次读行都引用同一份。把 TypeInfo 设计成"按引用传"避免了每行一次的克隆开销。
3.4.3 Value 与 ValueRef:数据持有权的分裂
这一对是整个读取链最微妙的设计。
sqlx-core/src/value.rs:9:
rust
pub trait Value {
type Database: Database<Value = Self>;
fn as_ref(&self) -> <Self::Database as Database>::ValueRef<'_>;
fn type_info(&self) -> Cow<'_, <Self::Database as Database>::TypeInfo>;
fn is_null(&self) -> bool;
// decode / try_decode 等方法
}sqlx-core/src/value.rs:99:
rust
pub trait ValueRef<'r>: Sized {
type Database: Database;
fn to_owned(&self) -> <Self::Database as Database>::Value;
fn type_info(&self) -> Cow<'_, <Self::Database as Database>::TypeInfo>;
fn is_null(&self) -> bool;
}Value 是拥有所有权的值——它独立存在,不借用 Row。ValueRef<'r> 是借用的值——生命周期 'r 挂到了 Row 上,意味着"只要 Row 还活着,我这个 ValueRef 就有效"。
为什么要两个? 想象 row.try_get::<i32, _>(0) 这个调用的流程:
try_get需要从第 0 列拿到一个原始值。- 原始值在 PostgreSQL 里可能只是
PgDataRow缓冲区里的几个字节——拷贝出来当 owned value 是浪费。 - 所以
Row::try_get_raw先返回一个ValueRef<'r>——只是一个指针 + 长度,生命周期绑到 row 上。 - 真正 decode 成
i32的时候,Decode::decode接收的是ValueRef,不是Value。
换句话说,ValueRef 是 decode 路径上的默认形态,Value 只在需要"把值抽出来跨行生存"时用。后者的典型用例是 query_scalar! 宏返回单列值——它内部会 ValueRef::to_owned() 成 Value,因为返回的值不能借用即将析构的 Row。
ValueRef::to_owned 的实现根据数据库有重大差异:
- Postgres / MySQL:"基本是一次引用计数递增,O(1)"(原文见
value.rs:105)——因为底层 buffer 用Arc<[u8]>或bytes::Bytes共享。 - SQLite:必须真实拷贝——SQLite 的
sqlite3_column_blob指针在 row 步进后就失效,没法靠引用计数保留。
这条差异很重要。它意味着你在 Postgres 驱动下做 let v: Value = row.value(0).to_owned(); 几乎零成本,在 SQLite 下则是一次 memcpy。第 7 章会专门讲这个差异如何影响 query! 的内部选择。
3.4.5 一次 row.try_get::<i32, _>(0) 的类型流
把前面几个关联类型串起来,看 let id: i32 = row.try_get(0)?; 这一行背后的类型动线:
关键节点:
try_get_raw先返回ValueRef<'_>——'_生命周期绑到&self(row)。编译器在这一步强制"ValueRef 不能活过 row"。Type::<i32, Postgres>::compatible做类型兼容检查——i32声明自己对应 "INT4" / "INT" / "SERIAL" 等,检查通过才进入 decode。这步失败会返回Error::Decode("mismatched type")。Decode::<i32, Postgres>::decode从ValueRef读字节——Postgres 的i32::decode是value.as_bytes_buffer().read_i32::<BigEndian>()(按大端读四字节)。- 整条路径零 owned 分配——
ValueRef借用 row 的 buffer,i32 是栈上 4 字节。对比如果你写let id: String = row.try_get(0)?,Decode 实现会ValueRef::as_str()?.to_owned()真做一次堆分配。
这张图解释了为什么 sqlx 号称"零拷贝解码"——不是指所有类型都没有拷贝,而是指只有必要时才拷贝(值类型零、引用类型 / 字符串按需)。这条路径的精妙处是它全部发生在 &self 的借用期内,不需要跨 await——这是 ValueRef 'r 生命周期的真正价值。
3.4.4 QueryResult:写操作的结果
rust
type QueryResult: 'static + Sized + Send + Sync + Default + Extend<Self::QueryResult>;这是唯一没有独立 trait 的关联类型——它是一个值类型(不是 trait bound)。PgQueryResult 是一个 struct 带 rows_affected: u64,MySQL 多一个 last_insert_id: u64,SQLite 多一个 changes: u64。没有公共 trait,只有公共的值蓝图。
关键是 Extend<Self::QueryResult> 这条 super-trait:PgQueryResult 实现 Extend<PgQueryResult> 意味着多个 QueryResult 可以"累加"成一个——这是 Executor::execute_many 流式执行多条语句时的合并需求。翻 sqlx-postgres/src/query_result.rs 能看到 impl Extend<PgQueryResult> for PgQueryResult 的实现——只是把 rows_affected 加起来。
3.5 写入链:Arguments / ArgumentBuffer / Statement
读取链完了,看写入链。
3.5.1 Arguments:参数集合
sqlx-core/src/arguments.rs:12:
rust
pub trait Arguments<'q>: Send + Sized + Default {
type Database: Database;
fn reserve(&mut self, additional: usize, size: usize);
fn add<T>(&mut self, value: T) -> Result<(), BoxDynError>
where T: 'q + Encode<'q, Self::Database> + Type<Self::Database>;
fn len(&self) -> usize;
fn format_placeholder<W: Write>(&self, writer: &mut W) -> fmt::Result { ... }
}Arguments<'q> 是一组待发送的参数。'q 生命周期挂在参数的潜在借用上——如果你 .bind(&some_string),那 some_string 的生命周期至少活到查询执行完,这条 bound 由 'q 守护。
注意 add 方法的 where T: 'q + Encode<'q, Self::Database> + Type<Self::Database> ——把 Encode 和 Type 绑在一起。这意味着你加一个参数时,系统同时要求"你能 encode 成字节"且"你声明了对应的数据库类型"。我们会在第 5 章详细看 Encode 和 Type 这一对。
format_placeholder 的默认实现是 ?——MySQL 和 SQLite 都用 ?,Postgres 要自定义为 $1 / $2 / ...(按参数位置带编号)。这条看似无关紧要的 API 决定了"同一份 Rust 代码能不能跨 DB 复用"——第 10 章 QueryBuilder 就靠它。
3.5.2 ArgumentBuffer:编码缓冲区
type ArgumentBuffer<'q>; 是唯一没有 trait bound 的关联类型——它没有 impl X<'q> 之类的约束。这意味着每家驱动可以完全自由地选择缓冲区形态:
Postgres:
PgArgumentBuffer(sqlx-postgres/src/arguments.rs:26-48)——一个带patches和type_holes的复杂结构:rustpub struct PgArgumentBuffer { buffer: Vec<u8>, count: usize, patches: Vec<Patch>, // 延迟补写(比如 `JSONB` 的变长前缀) type_holes: Vec<(usize, HoleKind)>, // OID 还没解析时先留洞,之后回填 }MySQL:
type ArgumentBuffer<'q> = Vec<u8>;—— 一个裸字节缓冲。没有复杂结构,因为 MySQL 的参数编码是一次性决定的(预处理阶段类型已经锁死)。SQLite:
type ArgumentBuffer<'q> = Vec<SqliteArgumentValue<'q>>;—— 一个带生命周期的值向量(SQLite 的参数不是按字节编码的,是按类型 tag + 值的变体存的)。
三家的 ArgumentBuffer 形态截然不同——这就是为什么没有共同 trait 约束。但这个关联类型仍然存在于 Database trait 里,因为 Arguments<'q>::ArgumentBuffer<'q> 被 Encode 实现引用:Encode::encode_by_ref(&self, buf: &mut DB::ArgumentBuffer<'_>)——每个 Encode 实现需要知道"我应该往哪种容器里写"。
3.5.3 Statement:预处理语句的抽象
sqlx-core/src/statement.rs:19:
rust
pub trait Statement<'q>: Send + Sync {
type Database: Database;
fn to_owned(&self) -> <Self::Database as Database>::Statement<'static>;
fn sql(&self) -> &str;
fn parameters(&self) -> Option<Either<&[<Self::Database as Database>::TypeInfo], usize>>;
fn columns(&self) -> &[<Self::Database as Database>::Column];
// ...
}Statement 代表已经被驱动 prepare 过的 SQL 语句——它缓存了 SQL 文本、参数类型、列元数据。Connection::prepare_with 返回的就是 Statement<'q>。
parameters 返回 Option<Either<&[TypeInfo], usize>> 这个怪异的类型,是因为不同数据库对"参数类型"的支持水平不同:
- Postgres:完整的参数类型——服务端在
Parse响应里返回每个$1、$2的 OID。所以是Either::Left(&[PgTypeInfo])。 - SQLite:只有参数个数——
sqlite3_bind_parameter_count(stmt)告诉你有几个?,但不告诉你每个?期望什么类型。所以是Either::Right(usize)。 - MySQL:介于两者之间——服务端说得模糊,所以 sqlx MySQL 驱动通常也返回
Either::Right(usize)。
这个 Either 类型是 sqlx 表达"能力差异"的一种手法——不是把每个驱动的 parameters 签名分叉,而是让统一签名返回一个枚举,调用方按 arm 分别处理。query! 宏的编译期校验就用这个差异:Postgres 下能检查每个参数的 Rust 类型匹不匹配 OID,SQLite 下只能检查参数个数匹不匹配。
3.6 元数据:TypeInfo
sqlx-core/src/type_info.rs:4:
rust
pub trait TypeInfo: Debug + Display + Clone + PartialEq<Self> + Send + Sync {
fn is_null(&self) -> bool;
fn name(&self) -> &str;
fn type_compatible(&self, other: &Self) -> bool
where Self: Sized { self == other }
fn is_void(&self) -> bool { false }
}TypeInfo 是数据库方的类型信息——"这一列是 INT4 还是 TEXT 还是 VARCHAR(255)"这种。不要和 Rust 侧的 Type<DB> trait 混淆——后者讲的是"Rust 的 i32 对应 DB 的哪个 TypeInfo"。这两者构成一对:
TypeInfo:DB → 自己对自己的描述。Type<DB>(第 5 章):Rust 类型 → DB TypeInfo 的映射。
Clone 和 PartialEq 是关键 super-trait——TypeInfo 经常需要按值传递、互相比较(比如"这一列的 TypeInfo 和我期望的是不是一致")。
具体实现差异:
- Postgres:
PgTypeInfo内部是PgType枚举 + 可选的 OID。PgType::Int4/PgType::Text/PgType::Custom(UStr)(用户自定义类型)。 - MySQL:
MySqlTypeInfo是ColumnType+char_set——因为 MySQL 的 VARCHAR 分字符集。 - SQLite:
SqliteTypeInfo是DataType枚举(Null/Int/Real/Text/Blob)——SQLite 动态类型系统,只有五种。
is_void 方法默认 false,唯一的例外是 Postgres 的 void 类型(SELECT pg_sleep(1) 的返回)——这个方法让 query! 宏知道"这列是 void 就别给 Rust 建一个字段"。
3.7 外层锚点:Connection / TransactionManager
这两个关联类型都在上一章已经见过——它们是 Database trait 引用另外两套 trait 家族的锚点。
type Connection: Connection<Database = Self>——PgConnection/MySqlConnection/SqliteConnection。第 12 章详细。type TransactionManager: TransactionManager<Database = Self>——PgTransactionManager/MySqlTransactionManager/SqliteTransactionManager。第 15 章详细。
它们的存在让 Database trait 不仅描述"数据格式",还描述"操作入口"——你从 DB::Connection 能获取连接,从 DB::TransactionManager 能开事务。所以 Database trait 是整个 driver 的类型级 manifest,读它就知道"这家驱动能提供什么"。
3.7.1 Connection 与 TransactionManager 的约束反转
这两个关联类型的约束用的是反向闭环:
rust
type Connection: Connection<Database = Self>;
type TransactionManager: TransactionManager<Database = Self>;第一行读作"我这个 Database 的 Connection 关联类型,必须指向一个具体的 PgConnection 之类,而那个 Connection 的 Database 关联类型又必须等于我"。
为什么这么绕?因为如果不加反向约束,下面这种病态实现就能编译:
rust
// 假设没有 Database = Self 约束
impl Database for Postgres {
type Connection = MySqlConnection; // 反手实现一个 MySQL 的 Connection
// ...
}那当用户调用 Pool::<Postgres>::acquire,底层调用 Postgres::Connection::open(),就会得到一个 MySQL 连接——类型系统彻底破产。Connection<Database = Self> 这条约束杜绝这种跨驱动张冠李戴。
这种"trait A 引用 trait B 的实现,要求 B 的关联类型回指 A"的模式叫做等式关联类型约束(associated type equality constraint),是 Rust trait 家族设计的基石。sqlx 的 11 个关联类型里每一个有 trait bound 的都带 Database = Self 约束,包括 Row<Database = Self>、Column<Database = Self>、Value<Database = Self>——这条约束像胶水把 11 个关联类型粘成一个不可拆的类型包。
TransactionManager 的反向闭环同理。它的 trait 签名(sqlx-core/src/transaction.rs:15):
rust
pub trait TransactionManager {
type Database: Database;
fn begin(conn: &mut <Self::Database as Database>::Connection) -> BoxFuture<'_, Result<(), Error>>;
fn commit(...) -> BoxFuture<'_, Result<(), Error>>;
fn rollback(...) -> BoxFuture<'_, Result<(), Error>>;
fn start_rollback(conn: &mut <Self::Database as Database>::Connection);
}注意 begin 的参数类型是 &mut <Self::Database as Database>::Connection——它从 TransactionManager::Database 跳回 Database::Connection。这条类型遍历经过两次关联类型投影(Database 拿到 Self::Database,再拿 Connection)。GAT 之前这会是一个 for<'c> HasConnection<'c> 之类的 helper trait,0.7 之后直接一行表达。
3.7.2 'static + Send + Debug 的传播
回到 Database 本身 trait Database: 'static + Sized + Send + Debug 的这四条 super-trait bound,它们通过关联类型系统传播到所有下游代码。举个具体的传播链:
Database: Send→ 要求DB::Connection: Connection<Database = Self>→Connection: Send(来自connection.rs:14的pub trait Connection: Send)→ 要求PgConnection: Send。Database: 'static→Row: 'static(来自row.rs:14的pub trait Row: Unpin + Send + Sync + 'static)→PgRow: 'static。Database: Debug→ 出现在tracing::error!("query on {DB:?} failed: {err}")的{DB:?}里。
这四条 bound 的作用是"告诉编译器:任何持有 DB: Database 的泛型代码都可以自动获得 DB::Connection: Send 等派生能力"——让你在 fn with_conn<DB: Database>(pool: &Pool<DB>) 里直接 tokio::spawn(pool.acquire()),而不用手写十条 DB::Connection: Send + 'static 的 where 子句。这是 super-trait bound 的传递性给上层带来的"一次声明,处处可用"收益。
3.8 关联常量:NAME 与 URL_SCHEMES
被绝大多数读者忽略的两行:
rust
const NAME: &'static str;
const URL_SCHEMES: &'static [&'static str];Postgres::NAME = "PostgreSQL",URL_SCHEMES = &["postgres", "postgresql"]MySql::NAME = "MySQL",URL_SCHEMES = &["mysql", "mariadb"]Sqlite::NAME = "SQLite",URL_SCHEMES = &["sqlite"]
这两个常量的用途有二:
- Any 驱动的 URL scheme 分发(第 2.6 节):
install_drivers注册的AnyDriver结构体就持有这两个值,AnyConnection::connect("postgres://...")路径里的 scheme 匹配直接用URL_SCHEMES.contains(&scheme)。 - 错误信息 / tracing span:
NAME出现在Error::Database的格式化输出里、tracing::info_span!("query", db = DB::NAME, ...)里。
这两个常量以"关联常量"而不是"关联类型"的形态存在,是因为它们的值不随实现方变化而必须同名——Postgres 的名字必须叫 "PostgreSQL"(URL scheme 依赖),不能叫别的。关联常量能在 const fn 里使用(这对 Any 驱动的 AnyDriver::without_migrate::<DB>() 是必要的,见 2.6.5 节)。
3.9 HasStatementCache:无方法标记 trait
sqlx-core/src/database.rs:113-114:
rust
/// A [`Database`] that maintains a client-side cache of prepared statements.
pub trait HasStatementCache {}一个 没有方法的 trait。Postgres 和 MySQL 实现它(impl HasStatementCache for Postgres {}、impl HasStatementCache for MySql {}),SQLite 不实现——因为 SQLite 驱动不做跨 fetch 的语句缓存,每次 sqlite3_prepare_v2 都新建。
这个 "标记 trait"(marker trait)的作用是把一项能力编码进类型系统。上层 API 用 where DB: HasStatementCache 守护"只有支持缓存的 DB 才能用这些方法"。
具体例子 1:Connection::cached_statements_size(connection.rs:126-132):
rust
fn cached_statements_size(&self) -> usize
where
Self::Database: HasStatementCache,
{
0
}这条 where 子句让 SqliteConnection::cached_statements_size() 编译期就不存在——你在 SQLite 连接上调用这个方法,编译器直接报"method not found",而不是运行时返回 0 后你疑惑为什么缓存永远 0。
具体例子 2:Query::persistent(query.rs:124-139):
rust
impl<'q, DB, A> Query<'q, DB, A>
where
DB: Database + HasStatementCache,
{
pub fn persistent(mut self, value: bool) -> Self { ... }
}persistent 方法只在 DB: HasStatementCache 时存在。SQLite 下你写 query("...").persistent(true) 会编译失败——因为 SQLite 不实现 HasStatementCache。
这种"把运行时能力差异搬到类型系统"的做法,是 Rust trait 家族设计里最精妙的一招。它避免了"在 SQLite 连接上传一个 persistent=true 被悄悄忽略"这类隐式降级——错误提前到编译期。
顺便提一下,Any 驱动(sqlx-core/src/any/database.rs:39)实现了 HasStatementCache——这不是因为 Any 总能缓存,而是 Any 需要保持和它"可能包装任一驱动"的泛用性一致,实际行为取决于内层具体驱动。
3.9.1 Rust 生态里的 marker trait 家谱
HasStatementCache 这种"无方法 trait 作为能力开关"的做法在 Rust 生态里有一整条谱系。把 sqlx 放进来对照:
| Marker trait | 属于哪个库 | 表达的能力 | 使用形态 |
|---|---|---|---|
Send | std | 可以跨线程转移所有权 | where T: Send |
Sync | std | &T 可以跨线程共享 | where T: Sync |
Unpin | std (通过 Pin) | 不受固定语义约束 | where T: Unpin |
Copy | std | 按位拷贝即可复制 | where T: Copy |
DerefMut | std | 可以派生可变引用 | where T: DerefMut |
HasStatementCache | sqlx-core | 支持客户端预处理语句缓存 | where DB: HasStatementCache |
DatabaseExt | sqlx-macros-core | 参与 query! 的编译期校验 | where DB: DatabaseExt |
TypeChecking | sqlx-core | 能把 DB 类型映射到 Rust 类型字符串 | where DB: TypeChecking |
Service (空 marker 版本) | tower(某些变体) | 某些中间件约束 | 不是真空 trait,这里只是形态对照 |
http_body::Body | hyper | 能当 HTTP body 流 | where B: Body |
观察几条规律:
- 无方法 marker 大多表达"能力开关"或"抽象断言"——它们不是告诉你"应该调什么方法",而是告诉类型系统"我有这项能力"。
HasStatementCache精确属于这一类。 - sqlx 的三个 marker(
HasStatementCache/DatabaseExt/TypeChecking)分布在两个 crate——HasStatementCache在 sqlx-core(用户可见),后两个在 sqlx-macros-core(宏内部)。这个拆分和 §2.3.1 讨论的 crate 边界一致。 - 没有"方法"的 trait 也可以继承有方法的 trait——比如
DatabaseExt: Database + TypeChecking(§2.3.1),它自身有 3 个方法但主要用法是作为"组合能力断言"。marker trait 的边界其实是模糊的:只要这个 trait 的主要使用方式是作 bound 而不是调方法,就属于这一族。
学会用 marker trait 表达能力差异,是 Rust trait 家族设计的基本功。HasStatementCache 这一行短短的 pub trait HasStatementCache {} 背后,代表的是一种"把动态 bool 搬到静态类型系统"的思路——从 SQLite 的 capability 缺失直接推导出"persistent(true) 在 SQLite 上编译不过"。
3.9.2 如果缺了某个关联类型会怎样
把 Database 的每一个关联类型做减法实验,看看少了它会坏在哪:
- 少了
Row:整条Executor::fetch路径断裂——BoxStream<'e, Result<DB::Row, Error>>签名写不出来。 - 少了
Column:Statement::columns返回类型丢失,query!的列类型映射没处落脚。 - 少了
TypeInfo:Type<DB>trait 的type_info()返回类型丢失;更严重的是第 5 章的 Encode/Decode 整套断了。 - 少了
Value:row.value(0)返回什么?无法抽取一个能跨行生存的值。 - 少了
ValueRef<'r>:try_get_raw断——必须先做to_owned()再 decode,所有解码都变成按值传递,性能大幅下降。 - 少了
Arguments<'q>:query(sql).bind(x)无处存值,Query 类型整个需要重做。 - 少了
ArgumentBuffer<'q>:Encode::encode_by_ref的 buf 参数类型丢失。 - 少了
Statement<'q>:Connection::prepare_with断,所有"用相同 SQL 多次查询"的优化消失。 - 少了
QueryResult:execute无法告诉用户"影响了几行"。 - 少了
TransactionManager:没法开事务,Connection::begin断。 - 少了
NAME:错误信息和 tracing span 缺数据库名——可以重建但麻烦。 - 少了
URL_SCHEMES:Any 驱动彻底废——无法按 URL scheme 分发。
这个减法实验本身不算新知识,但它证明了每一个关联类型都不是装饰——都对应着上层某一条具体的 API 路径或用户场景。这也回答了"为什么是 11 个不多不少"——sqlx 团队在 0.1 到 0.8 的演进里逐个加上来,每一个都因具体需求而生。
3.10 三家对照:Postgres / MySQL / SQLite 的 impl Database
把三家的 impl Database 并排摆一起(源文件分别是 sqlx-postgres/src/database.rs:14、sqlx-mysql/src/database.rs:12、sqlx-sqlite/src/database.rs:13):
| 关联类型 | Postgres | MySQL | SQLite |
|---|---|---|---|
Connection | PgConnection | MySqlConnection | SqliteConnection |
TransactionManager | PgTransactionManager | MySqlTransactionManager | SqliteTransactionManager |
Row | PgRow | MySqlRow | SqliteRow |
QueryResult | PgQueryResult | MySqlQueryResult | SqliteQueryResult |
Column | PgColumn | MySqlColumn | SqliteColumn |
TypeInfo | PgTypeInfo | MySqlTypeInfo | SqliteTypeInfo |
Value | PgValue | MySqlValue | SqliteValue |
ValueRef<'r> | PgValueRef<'r> | MySqlValueRef<'r> | SqliteValueRef<'r> |
Arguments<'q> | PgArguments | MySqlArguments | SqliteArguments<'q> |
ArgumentBuffer<'q> | PgArgumentBuffer | Vec<u8> | Vec<SqliteArgumentValue<'q>> |
Statement<'q> | PgStatement<'q> | MySqlStatement<'q> | SqliteStatement<'q> |
NAME | "PostgreSQL" | "MySQL" | "SQLite" |
URL_SCHEMES | &["postgres", "postgresql"] | &["mysql", "mariadb"] | &["sqlite"] |
HasStatementCache | 实现 | 实现 | 不实现 |
几条对比观察:
- 12 行关联类型 + 2 行常量 + 1 行 marker trait——三家结构完全平行,只是每个位置的具体类型不同。这就是 trait 家族设计的目标形态:等价替换。
Arguments<'q>的生命周期参数:Postgres 和 MySQL 是PgArguments和MySqlArguments(无'q),因为它们编码后就是字节 buffer 自包含了;SQLite 是SqliteArguments<'q>保留生命周期——因为 SQLite 的参数值(SqliteArgumentValue::Text(&str))可能借用外部字符串,不会预先序列化。ArgumentBuffer<'q>的形态差异最大——Postgres 的PgArgumentBuffer有 patch 和 type-hole 机制(延迟回填 OID),MySQL 直接裸字节,SQLite 是值向量。这揭示了三家协议的根本差异:Postgres 有 OID 的运行时查表、MySQL 有 COM_STMT_EXECUTE 的二进制格式、SQLite 有 C API 的"值按 tag 传"。后续第 16-18 章分别展开。- SQLite 唯一不实现
HasStatementCache——这决定了query("...").persistent(true)只在 Postgres / MySQL 下可用。
这张表不是让你背下来的——它是让你在后面读具体驱动实现时脑子里有一张"同构映射"的参照。当你读 sqlx-postgres/src/row.rs 看到 PgRow 时,你立刻知道它的位置对应 DB::Row,功能和 SqliteRow 等价——只是载荷格式不同。
3.11 本章小结
本章把 sqlx 整个类型体系的"收束点"Database trait 拆开,可以带走以下关键判断:
- 11 个关联类型 + 2 个关联常量 + 4 条 super-trait bound 共同描述了"一个 sqlx 驱动应该提供什么"——读取链(Row/Column/Value/ValueRef/QueryResult)、写入链(Arguments/ArgumentBuffer/Statement)、元数据(TypeInfo)、外层锚点(Connection/TransactionManager)、名字识别(NAME/URL_SCHEMES)。
- GAT(泛型关联类型)是让这套设计成立的语言前提(§3.3)。没有 GAT 的 sqlx 0.6 必须用
HasValueRef<'r>/HasArguments<'q>/HasStatement<'q>三个 helper trait +for<'r>HRTB 绕过;0.7 吃了一个 MSRV bump 到 1.65 来换得清爽的 trait 家族。 - GAT 的新代价是 object-safety 丢失——你不能
dyn Database。Any 驱动(§2.6)必须另起炉灶用AnyConnectionBackendtrait 绕过,这条代价预期之内。 - Value 与 ValueRef 的分裂(§3.4.3)反映了"数据持有权"的工程现实——ValueRef 是 decode 路径默认形态(零拷贝),Value 用于值需要跨 row 生存的场景。Postgres 的
to_owned是 O(1) 引用计数,SQLite 的to_owned必须 memcpy——差异在上层 API 不可见但性能可感。 - ArgumentBuffer 没有共同 trait(§3.5.2)——三家形态截然不同:Postgres 的
PgArgumentBuffer带 patch/hole、MySQL 是Vec<u8>、SQLite 是Vec<SqliteArgumentValue<'q>>。这是刻意不抽象的一处:协议差异太大,硬抽象只会让每家都被迫做运行时检查。 - HasStatementCache 是把能力编码进类型系统的标记 trait(§3.9)——Postgres / MySQL 实现,SQLite 不实现。
Query::persistent和Connection::cached_statements_size的where DB: HasStatementCache让"在 SQLite 下调这些方法"变成编译错误而不是运行时静默。 - NAME 与 URL_SCHEMES 两个常量(§3.8)让 Any 驱动可以在运行时按 URL scheme 分发,也让错误信息和 tracing span 能携带驱动名字。
- 三家对照表(§3.10)显示整个 trait 家族的"等价替换"结构——12 行平行,每家只是具体类型不同。这就是 trait 家族设计的终极目标。
3.11.1 判断题:如果换你设计
最后做几道"设计选择"判断题——把本章的理解直接转成工程直觉:
Q1:如果你要加一个 type Transaction<'c> 关联类型专门表示事务,让用户写 DB::Transaction<'c>::commit() 而不是 conn.begin().await?.commit().await?——这算好设计吗?
A:不算。Transaction<'c, DB> 作为一个具体 struct 已经够用(sqlx-core/src/transaction.rs:86),它本身不带驱动特有行为——三家 DB 的区别被 TransactionManager trait 消化。再加一层 type Transaction<'c> 关联类型会变成空壳,让 trait 家族膨胀无收益。判断原则:只有当"具体类型在不同 DB 下形态不同"时才值得加关联类型——Transaction 本身是泛型 struct,不满足这条。
Q2:如果把 NAME: &'static str 换成 fn name() -> &'static str 方法,行为等价吗?
A:等价,但 NAME 的关联常量形态能在 const fn 里用。回想 §2.6.5 里 AnyDriver::without_migrate::<DB>() 是 const fn——它要读 DB::NAME 和 DB::URL_SCHEMES。如果改成 fn name(),这条 const fn 路径会断(或者 Rust 的 const fn 能力要往前走一大步)。所以选择关联常量是被 Any 驱动的 const 注册路径倒推出来的。
Q3:ArgumentBuffer<'q> 无 trait bound 是偷懒吗?
A:不是偷懒,是不硬抽。这个缓冲区的形态在 Postgres / MySQL / SQLite 下差异是本质的——硬抽一个 trait Buffer { fn write_bytes(...); } 会让 SQLite 的 Vec<SqliteArgumentValue<'q>> 必须做一次 serialize-to-bytes-then-parse 的无谓开销。不抽,把表达权交给驱动,反而让每家 encode 路径最优。判断原则:抽象应当消除重复、而不是掩盖差异——后者往往导致泄漏。
Q4:为什么 TypeInfo 不叫 DbType 或 ColumnType?
A:因为 TypeInfo 承担的不只是"列类型"——它也被 Arguments::add 的类型检查、Value::type_info、Statement::parameters 共享。ColumnType 会狭化其语义;DbType 歧义——Rust 侧的 Type<DB> trait 也算"类型"。TypeInfo 这个名字精确对应"数据库方提供的类型描述信息",和 Rust 侧的 Type 一目了然区分。好的 trait 命名像好的变量命名——多花十分钟,长期收益巨大。
做完这四道题,你对 sqlx Database trait 的每一处选择就有了自己能解释的直觉,而不只是"背下来"。下一章进入 Executor 时,你会发现同样的设计权衡在不同 trait 上反复出现——这就是 trait 家族设计的整体性。
3.12 下一章指路
下一章,我们进入 Executor trait——Database trait 描述"驱动应该提供什么类型",Executor 描述"连接、Pool、Transaction 应该共同满足什么操作接口"。我们会看到 fetch / fetch_many / execute 这些方法如何围绕 DB: Database 构建、为什么 0.8 不再支持 impl Executor for &mut Transaction(本书第 1 章已经预告过,到第 4 章我们会从 trait 边界视角再看一遍)。