Database 与关联类型
主线位置:三条主线共用的类型表。查询主线里
sqlx::query("...")的类型是Query<'_, Postgres, PgArguments>,PgArguments就是<Postgres as Database>::Arguments,fetch_one返回的PgRow是Database::Row;连接池主线的Pool<Postgres>里放的是Database::Connection;编译期检查主线里describe返回Describe<DB>,列类型是Database::TypeInfo。这一页只讲这张表本身和它的两个替代方案(泛型约束、Any驱动),表里每一项的机制在各自的页面。
flowchart TB db["Postgres: Database"] --> conn[PgConnection] db --> row[PgRow] db --> args[PgArguments] db --> ti[PgTypeInfo] db --> vr["PgValueRef 'r"] db --> st[PgStatement] conn -->|"Database = Self"| db row -->|"Database = Self"| db args -->|"Database = Self"| db
一个数据库就是一组关联类型
Database 没有一个方法,只有关联类型和两个常量(每一项上面的 /// 文档注释都删掉了):
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: Arguments<Database = Self>;
type ArgumentBuffer;
type Statement: Statement<Database = Self>;
const NAME: &'static str;
const URL_SCHEMES: &'static [&'static str];
}
Postgres 驱动的实现就是把这张表填满:
impl Database for Postgres {
type Connection = PgConnection;
type TransactionManager = PgTransactionManager;
type Row = PgRow;
type QueryResult = PgQueryResult;
type Column = PgColumn;
type TypeInfo = PgTypeInfo;
type Value = PgValue;
type ValueRef<'r> = PgValueRef<'r>;
type Arguments = PgArguments;
type ArgumentBuffer = PgArgumentBuffer;
type Statement = PgStatement;
const NAME: &'static str = "PostgreSQL";
const URL_SCHEMES: &'static [&'static str] = &["postgres", "postgresql"];
}
Postgres 本身是个空结构体,从不被构造,只当类型参数用:Pool<Postgres>、Query<'_, Postgres, _>、Encode<'_, Postgres>。各项的用途和讲在哪:
| 关联类型 | Postgres | 在哪儿出现 | 详见 |
|---|---|---|---|
Connection | PgConnection | &mut PgConnection 是 Executor;池里存的就是它 | Executor 与 Acquire、连接选项与建立连接 |
TransactionManager | PgTransactionManager | Transaction 靠它发 BEGIN / SAVEPOINT | 事务 |
Row / Column | PgRow / PgColumn | fetch_* 的结果、FromRow 的输入 | Row 与 FromRow |
QueryResult | PgQueryResult | execute 的结果,只有 rows_affected;Extend 用来把多条语句的结果加起来 | 本页下文、Executor 与 Acquire |
TypeInfo | PgTypeInfo | Type::type_info()、列和参数的类型 | 类型、Postgres 类型 |
Value / ValueRef<'r> | PgValue / PgValueRef<'r> | Decode 的输入 | 类型 |
Arguments / ArgumentBuffer | PgArguments / PgArgumentBuffer | bind 往里写,Encode 的输出缓冲 | Query、QueryAs 与 SqlSafeStr |
Statement | PgStatement | prepare 的结果,带参数和列的元数据 | 扩展查询协议与语句缓存 |
两个常量也有消费者:Any 驱动登记一个驱动时,AnyDriver::without_migrate 直接取 DB::NAME 和 DB::URL_SCHEMES,后者决定哪个 URL 前缀交给哪个驱动(见本页最后一节)。
每一项都指回 Database
表里几乎每个 trait 都带 type Database 并要求等于 Self,Connection 反过来还要求 Database<Connection = Self>:
pub trait Connection: Send {
type Database: Database<Connection = Self>;
type Options: ConnectOptions<Connection = Self>;
// ...
}
双向约束让泛型代码从任何一端出发都能拿到整张表。只拿着一个 R: Row,就能写 <R::Database as Database>::ValueRef<'_>;只拿着一个 C: Connection,就能写 Transaction<'_, C::Database>。Database = Self 同时保证不会出现「PgRow 声称自己属于 SQLite」这种组合,编译器在 impl 处就拒绝。
TypeInfo 和 ArgumentBuffer 不指回来:TypeInfo 只要求能打印、比较、跨线程(Debug + Display + Clone + PartialEq + Send + Sync),ArgumentBuffer 连约束都没有,它只是 Encode::encode_by_ref(&self, buf: &mut DB::ArgumentBuffer) 的参数类型。
生命周期:只剩 ValueRef<'r>
0.9 的表里只有一个泛型关联类型 ValueRef<'r>。'r 是行缓冲的生命周期:PgValueRef<'r> 借用 PgRow 里那条 DataRow 的字节,解码成 &'r str 时不用拷贝。这和 serde 的 'de 是同一个思路,见 生命周期 'de 与借用。
0.8.6 的同一段还有三个:
/// The concrete `Arguments` implementation for this database.
type Arguments<'q>: Arguments<'q, Database = Self>;
/// The concrete type used as a buffer for arguments while encoding.
type ArgumentBuffer<'q>;
/// The concrete `Statement` implementation for this database.
type Statement<'q>: Statement<'q, Database = Self>;
'q 是查询的生命周期:参数缓冲可以借用被绑定的值,语句可以借用 SQL 字符串。Postgres 的 PgArguments 其实一直是自有数据(Encode 立刻把值写成字节),这个 'q 是为了给借用参数的驱动留余地。0.9 把它删了:Statement 改为持有一个自有的 SqlStr,Arguments 要求所有驱动的参数缓冲都自有数据。代价是 SQLite 驱动绑定 &str 时要拷贝一次,收益是 Query<'static, ...> 可以从函数里返回(实验见 Query、QueryAs 与 SqlSafeStr),也不再有一串 'q 跟着每个签名走。
0.8 之前没有 type Arguments<'q> 这种写法,Database 靠三个辅助 trait 作超 trait:for<'r> HasValueRef<'r, Database = Self>、for<'q> HasArguments<'q, Database = Self>、for<'q> HasStatement<'q, Database = Self>,带生命周期的类型放在它们里面,用的时候写 <DB as HasArguments<'q>>::Arguments。源码里当时有一句注释:「The upcoming Rust feature, Generic Associated Types, should obviate the need for this trait.」#2973(首次发布于 0.8.0)把三个辅助 trait 换成了 Database 里的 GAT,理由是「GATs have been stable since Rust 1.65」。
到 0.9.0,#3723「Removes the lifetime of Statement since the underlying sql string is now owned」,#3960 去掉 Arguments 和 ArgumentBuffer 的生命周期,PR 说明写的是「it standardizes that all argument buffers own their data」,并且让 query! 绑定参数时对引用的处理在所有驱动上「similar to the println! macro」。于是今天只剩 ValueRef<'r> 一个 GAT。
HasStatementCache
HasStatementCache 是一个空的标记 trait:
/// A [`Database`] that maintains a client-side cache of prepared statements.
pub trait HasStatementCache {}
它只出现在约束里:Query::persistent、QueryAs::persistent、QueryScalar::persistent 要求 DB: HasStatementCache,Connection::cached_statements_size 和 clear_cached_statements 也要求 Self::Database: HasStatementCache(这两个方法的默认实现分别返回 0 和什么也不做)。意思是:没有客户端语句缓存的驱动,连 .persistent(false) 这个开关都不该有。
0.9.0 里 Postgres、MySQL、SQLite 三个驱动和 Any 都实现了它,Any 那一行旁边注释写着「This may be true, depending on the selected database」。所以今天它不挡任何调用,只是给将来没有缓存的驱动留的位置。Postgres 的缓存怎么工作见 扩展查询协议与语句缓存。
写一个对所有数据库都成立的函数
关联类型的好处是零开销:Pool<Postgres> 上的调用单态化成直接调 PgConnection 的代码,没有虚表。代价落在泛型代码上:想写一个对任意 DB 都成立的函数,每一处用到的能力都要写成约束。
下面的函数对 Postgres 和 SQLite 都能用(src/bin/generic_db.rs,依赖是实验 crate 的 sqlx =0.9.0,开了 postgres、sqlite、macros 等 feature;平台 M2 Max、macOS、rustc 1.99.0、Postgres 16.14):
use sqlx::any::{AnyPoolOptions, install_default_drivers};
use sqlx::{Database, Encode, Executor, Pool, Type};
// Works for any driver: everything database-specific comes from DB's associated types.
async fn add_one<DB>(pool: &Pool<DB>, x: i64) -> anyhow::Result<i64>
where
DB: Database,
for<'c> &'c mut DB::Connection: Executor<'c, Database = DB>,
for<'q> i64: Encode<'q, DB> + Type<DB>,
DB::Arguments: sqlx::IntoArguments<DB>,
(i64,): for<'r> sqlx::FromRow<'r, DB::Row>,
{
let (y,): (i64,) = sqlx::query_as("select $1 + 1").bind(x).fetch_one(pool).await?;
Ok(y)
}
#[tokio::main]
async fn main() -> anyhow::Result<()> {
let url = std::env::var("DATABASE_URL")?;
let pg = sqlx::PgPool::connect(&url).await?;
let lite = sqlx::SqlitePool::connect("sqlite::memory:").await?;
println!("Postgres: {} ({})", add_one(&pg, 41).await?, <sqlx::Postgres as Database>::NAME);
println!("SQLite : {} ({})", add_one(&lite, 41).await?, <sqlx::Sqlite as Database>::NAME);
install_default_drivers();
for u in [url.as_str(), "sqlite::memory:"] {
let any = AnyPoolOptions::new().max_connections(1).connect(u).await?;
let backend = any.acquire().await?.backend_name().to_owned();
println!("Any -> {backend:<10}: {}", add_one(&any, 41).await?);
}
// ... 后面一段见本页的动脑筋
Ok(())
}
Postgres: 42 (PostgreSQL)
SQLite : 42 (SQLite)
Any -> PostgreSQL: 42
Any -> SQLite : 42
四条约束各对应函数体里的一个调用:
for<'c> &'c mut DB::Connection: Executor<'c, Database = DB>:&Pool<DB>的Executor实现要求这一条(见 Executor 与 Acquire)。Connectiontrait 本身不要求「&mut Self是Executor」,所以泛型代码只能自己补上。for<'q> i64: Encode<'q, DB> + Type<DB>:bind(x)的要求。DB::Arguments: IntoArguments<DB>:fetch_one所在的impl块要求参数类型能转成DB::Arguments。对具体驱动这是一条恒等转换,由impl_into_arguments_for_arguments!宏逐个驱动生成,宏上面的注释是「NOTE: required due to lack of lazy normalization」:没法写成一个对所有DB成立的 blanket impl,泛型代码里就看不到它,只能再写一遍。删掉这一行,rustc 报the following trait bounds were not satisfied: `<DB as sqlx::Database>::Arguments: IntoArguments<DB>`。(i64,): for<'r> FromRow<'r, DB::Row>:query_as把行转成元组。
一个 select $1 + 1 要四行约束,这就是「关联类型而不是 trait 对象」的代价。实际项目里常见的做法是不写泛型,直接用 PgPool;需要在运行时选数据库时用 Any。
为什么不用 trait 对象
能不能干脆存一个 Box<dyn Connection>?0.9.0 里不行:
use sqlx::postgres::{PgConnectOptions, Postgres};
use sqlx::Connection;
fn main() {
let _conns: Vec<Box<dyn Connection<Database = Postgres, Options = PgConnectOptions>>> = Vec::new();
}
error[E0038]: the trait `sqlx::Connection` is not dyn compatible
--> src/bin/dyn_conn.rs:5:27
|
5 | let _conns: Vec<Box<dyn Connection<Database = Postgres, Options = PgConnectOptions>>> = Vec::new();
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ `sqlx::Connection` is not dyn compatible
|
note: for a trait to be dyn compatible it needs to allow building a vtable
for more information, visit <https://doc.rust-lang.org/reference/items/traits.html#dyn-compatibility>
--> ~/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/sqlx-core-0.9.0/src/connection.rs:37:23
|
37 | fn close(self) -> impl Future<Output = Result<(), Error>> + Send + 'static;
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ the trait is not dyn compatible because method `close` references an `impl Trait` type in its return type
...
Connection 的异步方法在 0.9 改成了返回 impl Future(#3525「Remove unnecessary boxfutures」),省掉每次调用的一次堆分配,但带 impl Trait 返回值的 trait 不能做成 trait 对象。同一行代码在 0.8.6 里能编译,那时 close、ping、begin 都返回 BoxFuture。即使在 0.8.6,dyn Connection 也没多大用:执行查询的 Executor 是给 &mut PgConnection 这样的具体类型实现的,方法本身是泛型的(fn fetch_many<'e, 'q: 'e, E>(...)),带泛型参数的方法进不了虚表。
所以 sqlx 的分工是:编译期确定数据库时,全走关联类型和单态化;运行时才知道数据库时,用一个专门的驱动 Any,它自己也是一个 Database。
Any 驱动:在运行时补上这一块
Any 和 Postgres 一样填满这张表,只是填的都是「装着某个真实驱动」的类型:
impl Database for Any {
type Connection = AnyConnection;
// ...
type Arguments = AnyArguments;
type ArgumentBuffer = AnyArgumentBuffer;
type Statement = AnyStatement;
const NAME: &'static str = "Any";
const URL_SCHEMES: &'static [&'static str] = &[];
}
动态分派发生在连接这一层。AnyConnection 只有一个字段 backend: Box<dyn AnyConnectionBackend>,AnyConnectionBackend 是专门为 trait 对象设计的:方法返回 BoxFuture,close 收 self: Box<Self>,查询方法收的是具体的 AnyArguments 而不是泛型 E: Execute。Postgres 驱动给 PgConnection 实现这个 trait,在里面把 AnyArguments 转成 PgArguments、把 PgRow 转成 AnyRow。
参数和值的表示也换成了一个封闭的枚举。AnyArgumentBuffer 是 Vec<AnyValueKind>,AnyValueKind 只有这几种:
pub enum AnyValueKind {
Null(AnyTypeInfoKind),
Bool(bool),
SmallInt(i16),
Integer(i32),
BigInt(i64),
Real(f32),
Double(f64),
Text(Arc<String>),
TextSlice(Arc<str>),
Blob(Arc<Vec<u8>>),
}
这是所有驱动都能表示的公共子集。Postgres 一侧的映射在 TryFrom<&PgTypeInfo> for AnyTypeInfo:BOOL、INT2/4/8、FLOAT4/8、BYTEA、TEXT、VARCHAR、citext 有对应,其余一律返回 Error::AnyDriverError("Any driver does not support the Postgres type ...")。
运行时选驱动靠一张全局表。sqlx::any::install_default_drivers() 把按 feature 编译进来的驱动(MySQL、Postgres、SQLite 各自的 DRIVER 常量)放进 driver.rs 里的 static DRIVERS: OnceLock<&'static [AnyDriver]>,连接时按 URL 的 scheme 在表里找 url_schemes 包含它的那个驱动。不调 install_default_drivers 就连接,from_url 会 panic:「No drivers installed. Please see the documentation in sqlx::any for details.」
上面实验的后两行就是 Any 走通的样子:同一个 AnyPool 类型,URL 换一下就换了数据库,add_one 的四条约束对 Any 同样成立。
select now()?接着上面的程序,在同一个 Postgres URL 上建一个 AnyPool,执行一条 Postgres 自己当然能跑的查询(M2 Max、macOS、rustc 1.99.0、Postgres 16.14):
let any = AnyPoolOptions::new().max_connections(1).connect(&url).await?;
match sqlx::query("select now()").fetch_one(&any).await {
Ok(_) => println!("Any select now(): ok"),
Err(err) => println!("Any select now(): {err}"),
}
这里没有解码任何列,只是把行取回来。输出是 ok 还是一个错误?如果是错误,是哪一种?
动脑筋答案
Any select now(): error occurred while decoding column now: error in Any driver mapping: Any driver does not support the Postgres type PgTypeInfo(Timestamptz)
是错误,而且是在取行的时候,不是在解码的时候。PgConnection 的 AnyConnectionBackend 实现拿到 PgRow 后立刻转成 AnyRow,转换时逐列做 AnyTypeInfo::try_from(&col.type_info),TIMESTAMPTZ 不在映射表里,TryFrom<&PgColumn> for AnyColumn 把 AnyDriverError 包进 Error::ColumnDecode { index: "now", .. },所以外层写的是 error occurred while decoding column now。用户代码还没碰到这一列,整行就已经拿不回来了:Any 能处理的列类型就是 AnyValueKind 那十种。
小结
Database没有方法,是一张关联类型表;各个组件 trait 用Database = Self指回来,泛型代码从任一端都能拿到整张表。- 0.9 只剩
ValueRef<'r>一个 GAT:Statement持有自有的SqlStr,Arguments不再借用被绑定的值。 HasStatementCache是空标记,挡住没有语句缓存的驱动调用persistent;0.9.0 的四个Database都实现了它。- 写跨数据库的泛型函数要逐条补约束,
IntoArguments那条是因为没法写 blanket impl。 Connection在 0.9 不再是 dyn 兼容的;运行时选库用Any:Box<dyn AnyConnectionBackend>做分派,值收窄到AnyValueKind的十种,TIMESTAMPTZ这类列连取行都会失败。