一次 find 的全过程
主线位置:查询主线从头到尾。本页不深入任何一层,只把一次
cake::Entity::find_by_id(1).one(&db)经过的函数串起来,用抓包确认实际发出的 SQL 和参数,用连接池的计数确认每条语句单独借一次连接;每一段末尾链到详细讲它的那一章。
sequenceDiagram participant App as 调用方 participant Sel as Select 和 Selector participant Conn as DatabaseConnection participant Drv as 驱动 sqlx_postgres participant Pool as sqlx 连接池 participant PG as Postgres App->>Sel: find_by_id(1) Note over Sel: SelectStatement 加 WHERE App->>Sel: one(db) Note over Sel: limit(1) Sel->>Conn: query_one 语句树 Note over Conn: build 选方言 Conn->>Drv: query_one(Statement) Drv->>Pool: acquire Pool-->>Drv: PoolConnection Drv->>PG: Parse Bind Execute Sync PG-->>Drv: DataRow Drv-->>Sel: QueryResult Note over Sel: FromQueryResult Sel-->>App: Option Model Note over Drv,Pool: drop 时后台归还
实验:一张表、一次 find_by_id(1).one(&db)
实体用 2.0 的 dense 写法,表只有两列:
pub mod cake {
use sea_orm::entity::prelude::*;
#[sea_orm::model]
#[derive(Clone, Debug, PartialEq, Eq, DeriveEntityModel)]
#[sea_orm(table_name = "cake")]
pub struct Model {
#[sea_orm(primary_key)]
pub id: i32,
pub name: String,
}
impl ActiveModelBehavior for ActiveModel {}
}
库里是 create table so_intro.cake (id serial primary key, name text not null),插了一行 'cheese cake'。依赖锁在 sea-orm = "=2.0.4"(sqlx-postgres、runtime-tokio-rustls、macros、with-chrono、with-json)和 sea-query = "=1.0.2",没开 debug-print。
程序分两段。第一段不连库,只把同一个查询构造出来看一眼;第二段连接串指向一次查询的全过程里那个 tokio TCP 代理(sslmode=disable,池里最多 1 条连接,建池时的握手不打印),同时打开 sqlx 的语句日志。代理在 sqlx 卷的基础上多解了两样东西:Bind 里每个参数的字节,ParameterDescription 里每个参数的类型 OID。
#[tokio::main]
async fn main() -> Result<(), sea_orm::DbErr> {
tracing_subscriber::fmt()
.with_env_filter("sqlx=info")
.without_time()
.with_target(true)
.with_ansi(false)
.init();
setup().await?;
println!("--- 1. 不连库:build 与 to_string ---");
let stmt = cake::Entity::find_by_id(1).limit(1).build(DbBackend::Postgres);
println!("sql = {}", stmt.sql);
println!("values = {:?}", stmt.values);
let inlined = cake::Entity::find_by_id(1).limit(1).into_query().to_string(PostgresQueryBuilder);
println!("to_string = {inlined}");
let p = proxy::start("192.168.64.106:5432").await.map_err(|e| sea_orm::DbErr::Custom(e.to_string()))?;
let url = format!("postgres://lab:lab@127.0.0.1:{}/lab?sslmode=disable", p.port);
let mut opt = ConnectOptions::new(url);
opt
.max_connections(1)
.set_schema_search_path(SCHEMA)
.sqlx_logging(true)
.sqlx_logging_level(log::LevelFilter::Info);
p.quiet.store(true, Ordering::SeqCst);
let db = Database::connect(opt).await?;
p.quiet.store(false, Ordering::SeqCst);
for (round, id) in [("第一次", 1), ("第二次", 1), ("不存在的 id", 42)] {
println!("--- 2. {round} find_by_id({id}).one(&db) ---");
let model = cake::Entity::find_by_id(id).one(&db).await?;
println!("结果 {model:?}");
settle().await;
}
db.close().await
}
settle 睡 200 毫秒,让归还连接的后台任务在打印下一段标题之前跑完。M2 Max、macOS、rustc 1.99.0、Postgres 16.14 上的输出(去掉了建表时的一条 NOTICE):
--- 1. 不连库:build 与 to_string ---
sql = SELECT "cake"."id", "cake"."name" FROM "cake" WHERE "cake"."id" = $1 LIMIT $2
values = Some(Values([Int(Some(1)), BigUnsigned(Some(1))]))
to_string = SELECT "cake"."id", "cake"."name" FROM "cake" WHERE "cake"."id" = 1 LIMIT 1
INFO sqlx::query: summary="SET search_path = \"so_intro\"" db.statement="" rows_affected=0 rows_returned=0 elapsed=634.75µs elapsed_secs=0.00063475
--- 2. 第一次 find_by_id(1).one(&db) ---
[conn 1] C->S Sync
[conn 1] S->C ReadyForQuery(I)
[conn 1] C->S Parse(sqlx_s_1, "SELECT \"cake\".\"id\", \"cake\".\"name\" FROM \"cake\" WHERE \"cake\".\"id\" = $1 LIMIT $2"), Describe(S, sqlx_s_1), Sync
[conn 1] S->C ParseComplete, ParameterDescription(OID 23/20), RowDescription, ReadyForQuery(I)
[conn 1] C->S Bind(portal unnamed, stmt sqlx_s_1, $1=0x00000001 $2=0x0000000000000001), Execute, Close(P, unnamed), Sync
[conn 1] S->C BindComplete, DataRow(列长 4/11), CommandComplete("SELECT 1"), CloseComplete, ReadyForQuery(I)
INFO sqlx::query: summary="SELECT \"cake\".\"id\", \"cake\".\"name\" FROM …" db.statement="\n\nSELECT \"cake\".\"id\", \"cake\".\"name\" FROM \"cake\" WHERE \"cake\".\"id\" = $1 LIMIT $2\n" rows_affected=1 rows_returned=1 elapsed=3.035167ms elapsed_secs=0.003035167
结果 Some(Model { id: 1, name: "cheese cake" })
[conn 1] C->S Sync
[conn 1] S->C ReadyForQuery(I)
--- 2. 第二次 find_by_id(1).one(&db) ---
[conn 1] C->S Sync
[conn 1] S->C ReadyForQuery(I)
[conn 1] C->S Bind(portal unnamed, stmt sqlx_s_1, $1=0x00000001 $2=0x0000000000000001), Execute, Close(P, unnamed), Sync
[conn 1] S->C BindComplete, DataRow(列长 4/11), CommandComplete("SELECT 1"), CloseComplete, ReadyForQuery(I)
INFO sqlx::query: summary="SELECT \"cake\".\"id\", \"cake\".\"name\" FROM …" db.statement="\n\nSELECT \"cake\".\"id\", \"cake\".\"name\" FROM \"cake\" WHERE \"cake\".\"id\" = $1 LIMIT $2\n" rows_affected=1 rows_returned=1 elapsed=954.333µs elapsed_secs=0.000954333
结果 Some(Model { id: 1, name: "cheese cake" })
[conn 1] C->S Sync
[conn 1] S->C ReadyForQuery(I)
--- 2. 不存在的 id find_by_id(42).one(&db) ---
[conn 1] C->S Sync
[conn 1] S->C ReadyForQuery(I)
[conn 1] C->S Bind(portal unnamed, stmt sqlx_s_1, $1=0x0000002a $2=0x0000000000000001), Execute, Close(P, unnamed), Sync
[conn 1] S->C BindComplete, CommandComplete("SELECT 0"), CloseComplete, ReadyForQuery(I)
INFO sqlx::query: summary="SELECT \"cake\".\"id\", \"cake\".\"name\" FROM …" db.statement="\n\nSELECT \"cake\".\"id\", \"cake\".\"name\" FROM \"cake\" WHERE \"cake\".\"id\" = $1 LIMIT $2\n" rows_affected=0 rows_returned=0 elapsed=943.666µs elapsed_secs=0.000943666
结果 None
[conn 1] C->S Sync
[conn 1] S->C ReadyForQuery(I)
从这段输出能直接读出几件事:
- 实际发出的 SQL 是
SELECT "cake"."id", "cake"."name" FROM "cake" WHERE "cake"."id" = $1 LIMIT $2:实体的每一列都按"表"."列"列出来,没有*;LIMIT也是一个绑定参数,不是写进 SQL 的字面量1。 - 两个参数的类型:
ParameterDescription(OID 23/20),$1是int4,$2是int8。Bind里$1是 4 个字节的 1(第三次是0x2a,即 42),$2是 8 个字节的 1。 to_string打出来的... = 1 LIMIT 1把值内联进了 SQL,那不是发出去的东西,只能用来读。- 第二次执行没有
Parse,Bind直接引用sqlx_s_1:sea-orm 每次生成的 SQL 一字不差,命中了 sqlx 按连接的语句缓存。 - 每次查询前后各有一对孤零零的
Sync/ReadyForQuery,是 sqlx 连接池借出和归还时的 ping;sea-orm 的ConnectOptions默认test_before_acquire为true,原样交给 sqlx。 id = 42没有DataRow,CommandComplete("SELECT 0"),结果是None而不是错误。
下面按调用顺序把这些对到源码上。
1. find_by_id:造一个 Select
EntityTrait::find_by_id 是 EntityTrait 的默认方法:
fn find_by_id<T>(values: T) -> Select<Self>
where
T: Into<<Self::PrimaryKey as PrimaryKeyTrait>::ValueType>,
{
let mut select = Self::find();
let mut keys = Self::PrimaryKey::iter();
for v in values.into().into_value_tuple() {
if let Some(key) = keys.next() {
let col = key.into_column();
select = select.filter(col.eq(v));
} else {
unreachable!("primary key arity mismatch");
}
}
select
}
主键值先转成一个值元组,和 PrimaryKey 枚举的变体一一配对,每对加一个 col.eq(v) 过滤。单列主键就是一个 WHERE "cake"."id" = $1,复合主键就是几个 AND。T 的约束是 Into<ValueType>,ValueType 是宏从主键字段的类型推出来的(这里是 i32),所以 find_by_id("1") 编译不过。主键 trait 和 ValueType 见实体的几个 trait。
EntityTrait::find 就是 Select::new(),Select::new 建一个空的 sea-query SelectStatement,再填两样东西:
pub(crate) fn new() -> Self {
Self {
query: SelectStatement::new(),
entity: PhantomData,
linked_index: 0,
}
.prepare_select()
.prepare_from()
}
fn prepare_select(mut self) -> Self {
self.query.exprs(self.column_list());
self
}
fn column_list(&self) -> Vec<SelectExpr> {
E::Column::iter()
.map(ColumnSelectExt::into_select_expr)
.collect()
}
E::Column::iter() 遍历宏生成的 Column 枚举,每个变体变成一个 SelectExpr,这就是 SQL 里那串 "cake"."id", "cake"."name";prepare_from 填 FROM "cake"。Select<E> 自己只是 SelectStatement 加一个 PhantomData<E>(外加一个计数字段 linked_index):实体类型只在编译期起作用,运行时手里就是一棵 sea-query 的语句树。到这里还没有 SQL 字符串,也没有碰数据库。filter、column、order_by 这些方法见 Select 与过滤,语句树的结构见语句是一棵树。
2. one:换成 Selector,加 LIMIT
Select::one 只有一行 self.into_model().one(db).await。into_model 把语句原样搬进一个 Selector<SelectModel<M>>,M 是 E::Model。Selector 和 Select 装的是同一个 SelectStatement,区别在类型参数:S: SelectorTrait 决定每一行解码成什么(Model、元组、JSON、自定义 struct),见 Select 与过滤。
pub async fn one<C>(mut self, db: &C) -> Result<Option<S::Item>, DbErr>
where
C: ConnectionTrait,
{
self.query.limit(1);
let row = db.query_one(&self.query).await?;
match row {
Some(row) => Ok(Some(S::from_raw_query_result(row)?)),
None => Ok(None),
}
}
limit(1) 是 sea-orm 自己加的,不管调用方写没写。这一句有实际作用:sqlx 的 fetch_one 并不会读到一行就停,服务端返回多少行它就读多少行(0.8.4 起 Execute 一律发 limit: 0,见一次查询的全过程的冷知识)。没有 SQL 里的 LIMIT,Entity::find().one(&db) 会让服务端把整张表发过来、客户端全部读完再扔掉。
sea-query 的 limit 把 u64 存成一个 Value:self.limit = Some(limit.into()),也就是 Value::BigUnsigned(Some(1)),第一段输出的 values 里能看到它。写 SQL 时,prepare_select_limit_offset 对它调 prepare_value,和 WHERE 里的值走同一条路:
fn prepare_select_limit_offset(&self, select: &SelectStatement, sql: &mut impl SqlWriter) {
if let Some(limit) = &select.limit {
sql.write_str(" LIMIT ").unwrap();
self.prepare_value(limit.clone(), sql);
}
if let Some(offset) = &select.offset {
sql.write_str(" OFFSET ").unwrap();
self.prepare_value(offset.clone(), sql);
}
}
所以 LIMIT 成了 $2。哪些值变成参数、哪些直接写进 SQL,见 QueryBuilder 与参数。
问: LIMIT 1 永远是 1,做成参数不是白多一个参数吗?
答: 做成参数,SQL 原文就和 limit 的值无关。find_by_id(1).one(&db)、find_by_id(42).one(&db)、find_by_id(7).limit(10).all(&db) 的 SQL 文本都是同一条 ... WHERE "cake"."id" = $1 LIMIT $2,在一条连接上只 Parse 一次。sqlx 的语句缓存按 SQL 原文做键(见一次查询的全过程的动脑筋),如果 limit 内联成字面量,每换一个分页大小就是一条新语句。上面输出里第三次换了 id 也没有 Parse,就是这个原因。代价只是 Bind 里多 8 个字节。
3. query_one:到这里才选方言
db 是 &DatabaseConnection,query_one 是 ConnectionTrait 的一个默认方法:
/// Execute a [`StatementBuilder`] and return a single row of `QueryResult`
async fn query_one<S: StatementBuilder>(&self, stmt: &S) -> Result<Option<QueryResult>, DbErr> {
let db_backend = self.get_database_backend();
let stmt = db_backend.build(stmt);
self.query_one_raw(stmt).await
}
它收的是语句树(&SelectStatement),不是 SQL 字符串。get_database_backend() 问连接是哪种数据库,DbBackend::build 转给 StatementBuilder::build。SelectStatement 的 StatementBuilder 实现由 build_query_stmt! 生成,按后端挑一个 sea-query 的 QueryBuilder:
macro_rules! build_any_stmt {
($stmt: expr, $db_backend: expr) => {
match $db_backend {
DbBackend::MySql => $stmt.build(MysqlQueryBuilder),
DbBackend::Postgres => $stmt.build(PostgresQueryBuilder),
DbBackend::Sqlite => $stmt.build(SqliteQueryBuilder),
}
};
}
PostgresQueryBuilder 是一个单元结构体,build 返回 (String, Values):SQL 里的值都换成 $1、$2,值按出现顺序放进 Values。prepare_select_statement 按 SELECT、FROM、JOIN、WHERE、GROUP BY、HAVING、ORDER BY、LIMIT / OFFSET 的顺序逐个子句写。结果包进 sea-orm 的 Statement:sql、values: Some(Values)、db_backend 三个字段,第一段输出里打印的就是它。
同一棵语句树,直到这一步才决定写成哪种方言。Select 本身不知道会跑在 Postgres 上,这也是不连库就能 build(DbBackend::Postgres) 看 SQL 的原因。QueryBuilder 怎么逐个子句写、Postgres 覆盖了哪些方法,见 QueryBuilder 与参数。
query_one 在 2.0 之前收的是 Statement1.1.20 里 ConnectionTrait 的方法是 async fn query_one(&self, stmt: Statement),调用方自己先 build:那时的 Selector::one 在 limit(1) 之后调 into_selector_raw(db),里面 db.get_database_backend().build(&self.query),再把 Statement 交给连接。PR #2657(「Query raw API」,2025-07-21 合并)把 execute / query_one / query_all / stream 改成收 sea-query 语句,另加一组收 Statement 的 execute_raw / query_one_raw / query_all_raw / stream_raw。PR 的说明是「Make the connection API accepts more abstract QueryStatements instead of raw Statement. Users don't have to build manually, and have to explicitly use the _raw methods for raw SQL. This is in preparation for access control.」最后一句指的是 2.0 的 RBAC:StatementBuilder 在 rbac feature 下多一个 audit(),RestrictedConnection 覆盖 query_one,先 user_can_run(stmt) 用 audit() 列出这棵语句树要对哪些表做什么操作、逐条查权限,再转给真正的连接,而它的 query_one_raw 直接返回 RbacError("Raw query is not supported: ..."):一段 SQL 字符串没法可靠地审查,一棵语句树可以。这项改动合入时 2.0 还在候选版本阶段(仓库里没有那个时期的 rc tag),CHANGELOG 把它记在 2.0.0 的破坏性改动里,正式随 2.0.0(2026-07-19)发布。
4. DatabaseConnection 分派到驱动
DatabaseConnection 在 2.0 是一个 struct,真正的连接在 inner: DatabaseConnectionType 里,DatabaseConnectionType 是按 feature 打开分支的 enum(Postgres、MySQL、SQLite、rusqlite、mock、proxy,外加一个 Disconnected)。query_one_raw 就是一个 match:
async fn query_one_raw(&self, stmt: Statement) -> Result<Option<QueryResult>, DbErr> {
super::tracing_spans::with_db_span!(
"sea_orm.query_one",
self.get_database_backend(),
stmt.sql.as_str(),
record_stmt = self.get_record_stmt_in_spans(),
async {
match &self.inner {
// ...
#[cfg(feature = "sqlx-postgres")]
DatabaseConnectionType::SqlxPostgresPoolConnection(conn) => {
conn.query_one(stmt).await
}
// ...
DatabaseConnectionType::Disconnected => Err(conn_err("Disconnected")),
}
}
)
}
外面包的 with_db_span! 在开了 tracing-spans feature 时为每条语句建一个 sea_orm.query_one span,没开时什么也不做(span 怎么建见 tracing 卷的 Span 与 Event)。分派是运行时的 match,不是泛型:同一个 DatabaseConnection 类型可以装任何一种后端,代价是每条语句多一次分支,以及打开 mock feature 会改变这个 enum 的形状。DatabaseConnection、ConnectOptions 见 DatabaseConnection 与 ConnectOptions。
5. 驱动:sqlx_query、acquire、fetch_one
SqlxPostgresPoolConnection 里存的就是一个 sqlx::PgPool。它的 query_one:
pub async fn query_one(&self, stmt: Statement) -> Result<Option<QueryResult>, DbErr> {
debug_print!("{}", stmt);
let query = sqlx_query(&stmt);
let mut conn = self.pool.acquire().await.map_err(sqlx_conn_acquire_err)?;
crate::metric::metric!(self.metric_callback, &stmt, {
match query.fetch_one(&mut *conn).await {
Ok(row) => Ok(Some(row.into())),
Err(err) => match err {
sqlx::Error::RowNotFound => Ok(None),
_ => Err(sqlx_error_to_query_err(err)),
},
}
})
}
四件事,按顺序:
debug_print!:开了debug-printfeature 时是一条tracing::debug!,打印Statement的Display(值内联进 SQL,见怎么读这一卷的实验环境);没开时什么也不做。sqlx_query把Statement变成 sqlx 的查询:pub(crate) fn sqlx_query(stmt: &Statement) -> sqlx::query::Query<'_, Postgres, SqlxValues> { let values = stmt .values .as_ref() .map_or(Values(Vec::new()), |values| values.clone()); sqlx::query_with(sqlx::AssertSqlSafe(stmt.sql.as_str()), SqlxValues(values)) }AssertSqlSafe是 sqlx 0.9 要求的标记:query_with只收SqlSafeStr,运行期拼出来的String要显式声明「这段 SQL 是安全的」(见 Query、QueryAs 与 SqlSafeStr)。sea-orm 敢这样声明,是因为 SQL 里的值都已经变成了$N。参数是 sea-query-sqlx 的SqlxValues,它实现sqlx::IntoArguments,逐个Value分支add进PgArguments。Value::Int(Some(1))加的是i32,所以$1的 OID 是 23;Value::BigUnsigned分支是<i64 as TryFrom<u64>>::try_from(i).unwrap(),Postgres 没有无符号整数,u64转成i64再绑,所以$2是 OID 20(int8)、8 个字节。各分支见绑到 sqlx。self.pool.acquire():每条语句单独向池借一条连接,借不到时PoolTimedOut被sqlx_conn_acquire_err换成DbErr::ConnectionAcquire(ConnAcquireErr::Timeout)。输出里每段开头那对Sync/ReadyForQuery就是借出时的 ping。下一节专门看这一步。query.fetch_one(&mut *conn):执行器是借来的这条&mut PgConnection,不是&Pool。从这里往下全是 sqlx 卷的查询主线:查语句缓存,第一次Parse+Describe+Sync,然后Bind+Execute+Close+Sync,见一次查询的全过程。没有行时 sqlx 返回RowNotFound,sea-orm 把它变成Ok(None),其他错误包成DbErr::Query(见错误)。
fetch_one 返回后,conn(一个 PoolConnection)在函数末尾被 drop,sqlx 在后台 spawn 一个任务把它还回池里,那就是每段末尾那对 ping(见归还与维护)。
6. 每条语句单独借一次连接
上一节第 3 步说的是「每条语句」,不是「每个 DatabaseConnection」。DatabaseConnection 不持有任何一条连接,只持有池。验证它用 2.0.4 暴露出来的 get_postgres_connection_pool,拿到 &sqlx::PgPool 之后看 size()(池里现有的连接数)和 num_idle()(其中空闲的)。这次直连,不经代理,池最多 2 条连接,acquire_timeout 1 秒:
let mut opt = ConnectOptions::new(DIRECT);
opt
.max_connections(2)
.acquire_timeout(Duration::from_secs(1))
.set_schema_search_path(SCHEMA)
.sqlx_logging(false);
let db = Database::connect(opt).await?;
let pool = db.get_postgres_connection_pool();
let show = |label: &str| println!("{label}:size={} idle={}", pool.size(), pool.num_idle());
show("connect 之后");
// one statement, observed right away and after the spawned return task ran
cake::Entity::find_by_id(1).one(&db).await?;
show("one 返回时");
settle().await;
show("200ms 之后");
// ... 中间一段见本页的动脑筋
// hold every connection of the pool, then call one()
let held1 = pool.acquire().await.expect("acquire 1");
let held2 = pool.acquire().await.expect("acquire 2");
show("占住两条之后");
let started = std::time::Instant::now();
let res = cake::Entity::find_by_id(1).one(&db).await;
if let Err(e) = &res { println!("Display:{e}"); }
println!("one = {res:?},用时 {:.1}s", started.elapsed().as_secs_f64());
drop(held1);
drop(held2);
settle().await;
let res = cake::Entity::find_by_id(1).one(&db).await;
println!("还回去之后 one = {res:?}");
输出(同一个程序前后跑了五次,除了后端进程号,每次都一样):
connect 之后:size=1 idle=1
one 返回时:size=1 idle=0
200ms 之后:size=1 idle=1
...
占住两条之后:size=2 idle=0
Display:Failed to acquire connection from pool: Connection pool timed out
one = Err(ConnectionAcquire(Timeout)),用时 1.0s
还回去之后 one = Ok(Some(Model { id: 1, name: "cheese cake" }))
Database::connect默认不是懒连接,建池时先连好一条,size=1 idle=1。one刚返回时idle=0:连接还没回到空闲队列。归还发生在 sqlx spawn 出来的任务里,调用方拿到结果时它可能还没跑完;200 毫秒后idle=1,连接回来了。- 把池里两条连接都用 sqlx 的
pool.acquire()占住,one就等满acquire_timeout后失败,错误是ConnectionAcquire(Timeout)。它没有任何办法用到调用方手里那两条连接,因为&DatabaseConnection不知道它们的存在。还回去之后,同样的调用立刻成功。
这条规则的后果,后面几章会反复用到:两条 find 之间连接已经回到池里,下一条可能落在另一条连接上,所以 SET 之类的会话设置在 &DatabaseConnection 上没有意义(set_schema_search_path 是在 sqlx 的 after_connect 里对每条新连接执行一次 SET search_path,上面第一段输出里那条日志就是它);要让几条语句在同一条连接上,就开事务,DatabaseTransaction 持有一条连接,见事务。acquire 内部怎么拿许可、什么时候新建连接,见 sqlx 卷的获取连接。
还是上面那个池(max_connections(2),建池后有 1 条空闲连接,one 之后等了 200 毫秒),中间那段是:
// which backend runs each statement
let pid = Statement::from_string(DbBackend::Postgres, "select pg_backend_pid() as pid");
for i in 1..=3 {
let row = db.query_one_raw(pid.clone()).await?.expect("one row");
let pid: i32 = row.try_get("", "pid")?;
println!("第 {i} 条 pg_backend_pid = {pid}");
}
三条语句一条接一条 await,中间没有任何等待。pg_backend_pid() 返回执行这条语句的服务端进程号,一条连接对应一个进程。三行输出里有几个不同的进程号?
7. 行变成 Model:QueryResult 和 FromQueryResult
驱动拿回的 PgRow 经 From<PgRow> for QueryResult 包成 QueryResult { row: QueryResultRow::SqlxPostgres(row) }。QueryResult 和驱动无关,里面的 QueryResultRow 按 feature 分支装 sqlx 的 PgRow / MySqlRow / SqliteRow、mock 或 proxy 的行,后面所有解码都只看 QueryResult。
回到 Selector::one,S::from_raw_query_result(row) 对 SelectModel<M> 的实现是 M::from_query_result(&res, ""),第二个参数是列名前缀,单表查询为空(join 时是 A_ / B_ 之类,见 join 与 find_related)。cake::Model 的 FromQueryResult 是 DeriveEntityModel 生成的,cargo expand 里的关键几行:
fn from_query_result_nullable(
row: &sea_orm::QueryResult,
pre: &str,
) -> std::result::Result<Self, sea_orm::TryGetError> {
let id = row
.try_get_nullable::<
Option<i32>,
>(
pre,
sea_orm::IdenStatic::as_str(
&<<Self as sea_orm::ModelTrait>::Entity as sea_orm::entity::EntityTrait>::Column::Id,
)
.into(),
)?;
let name = row
// ...
每个字段按列名(Column::Id 的字符串 "id",加上前缀)从行里取,类型由 TryGetable 决定,最后拼成 Model { id, name }。按名字取,所以 SQL 里列的顺序无所谓,但列名必须对得上:select_only 漏了列、column_as 起了别的名字,解码就在这一步失败。生成代码的全貌见 DeriveEntityModel 与 dense 格式,TryGetable 和 NULL 的处理见列与值,自定义结果类型见部分列与自定义结果。
对照:查询主线的每一步
| 步骤 | 源码 | 讲它的页 |
|---|---|---|
1. find_by_id 按主键加 col.eq(v);find = Select::new,列出实体每一列、填 FROM | entity/base_entity.rs、query/select.rs | 实体的几个 trait、Select 与过滤 |
2. one → into_model → Selector::one:limit(1),db.query_one(&self.query) | executor/select.rs | Select 与过滤 |
3. ConnectionTrait::query_one:get_database_backend() + build → PostgresQueryBuilder 写出 SQL 和 Values → Statement | database/connection.rs、database/statement.rs、sea-query backend/ | 语句是一棵树、QueryBuilder 与参数 |
4. DatabaseConnection::query_one_raw 按 inner 分派 | database/db_connection.rs | DatabaseConnection 与 ConnectOptions |
5. sqlx_query = query_with(AssertSqlSafe(sql), SqlxValues(values)) | driver/sqlx_postgres.rs、sea-query-sqlx | 绑到 sqlx |
6. pool.acquire(),每条语句一次 | driver/sqlx_postgres.rs | DatabaseConnection 与 ConnectOptions、sqlx 卷获取连接 |
7. fetch_one(&mut *conn):语句缓存、Parse / Bind / Execute;RowNotFound → Ok(None) | sqlx | sqlx 卷一次查询的全过程、错误 |
8. PgRow → QueryResult → SelectModel → Model::from_query_result | driver/sqlx_postgres.rs、executor/query.rs、宏生成的代码 | DeriveEntityModel 与 dense 格式、列与值 |
动脑筋答案
完整输出(中间三行就是那段代码打的):
connect 之后:size=1 idle=1
one 返回时:size=1 idle=0
200ms 之后:size=1 idle=1
第 1 条 pg_backend_pid = 1328
第 2 条 pg_backend_pid = 1329
第 3 条 pg_backend_pid = 1328
占住两条之后:size=2 idle=0
Display:Failed to acquire connection from pool: Connection pool timed out
one = Err(ConnectionAcquire(Timeout)),用时 1.0s
还回去之后 one = Ok(Some(Model { id: 1, name: "cheese cake" }))
两个。五次运行的进程号分别是 1224 / 1225 / 1224、1228 / 1229 / 1228、1231 / 1232 / 1231、1234 / 1235 / 1234、1328 / 1329 / 1328,模式完全一样:
- 第 1 条借走池里唯一一条空闲连接(进程 1328)。语句结束时这条连接被 drop,归还在后台任务里进行。
- 第 2 条紧接着
acquire,这时那条连接还没回到空闲队列(就像上面one 返回时的idle=0),池里size=1小于max_connections2,于是新建了一条(进程 1329)。 - 第 3 条来的时候,第一条连接已经归还,第二条还在归还途中,借到的又是 1328。
所以「紧挨着的两条语句」不但不保证在同一条连接上,在一个不满的池里反而很可能不在。依赖同一条连接的东西(会话变量、临时表、pg_advisory_lock)都只能放进事务。
小结
find_by_id(1)只造语句树:Select<E>里装的是SelectStatement和PhantomData<E>,列出实体的每一列,按主键加WHERE。Selector::one总是先limit(1),因为 sqlx 的fetch_one会读完服务端发来的所有行;LIMIT和WHERE里的值一样变成绑定参数($2,u64转i64绑成int8),SQL 原文和值无关,语句缓存能命中。ConnectionTrait::query_one收语句树,在这里才按后端选QueryBuilder写出 SQL;2.0 起这样设计是为了 RBAC 能审查语句(PR #2657)。- 驱动里是
sqlx::query_with(AssertSqlSafe(sql), SqlxValues(values)),然后每条语句pool.acquire()一次,再交给 sqlx 的fetch_one;RowNotFound变成Ok(None)。 - 连续的语句可能落在不同的连接上;
DatabaseConnection只持有池,要一条连接就开事务。 - 行解码按列名:
QueryResult→Model::from_query_result,由DeriveEntityModel生成。