Rust in Everything
基于 sea-orm 2.0.4

一次 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 与过滤。

Selector::one:

    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 与参数。

📝NOTE
没有蠢问题

问: 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 与参数。

📝NOTE
冷知识:query_one 在 2.0 之前收的是 Statement

1.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)),
                },
            }
        })
    }

四件事,按顺序:

  1. debug_print!:开了 debug-print feature 时是一条 tracing::debug!,打印 Statement 的 Display(值内联进 SQL,见怎么读这一卷的实验环境);没开时什么也不做。

  2. 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。

  3. self.pool.acquire():每条语句单独向池借一条连接,借不到时 PoolTimedOut 被 sqlx_conn_acquire_err 换成 DbErr::ConnectionAcquire(ConnAcquireErr::Timeout)。输出里每段开头那对 Sync / ReadyForQuery 就是借出时的 ping。下一节专门看这一步。

  4. 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 卷的获取连接。

❗IMPORTANT
动脑筋:三条紧挨着的语句,落在几个后端进程上?

还是上面那个池(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,列出实体每一列、填 FROMentity/base_entity.rs、query/select.rs实体的几个 trait、Select 与过滤
2. one → into_model → Selector::one:limit(1),db.query_one(&self.query)executor/select.rsSelect 与过滤
3. ConnectionTrait::query_one:get_database_backend() + build → PostgresQueryBuilder 写出 SQL 和 Values → Statementdatabase/connection.rs、database/statement.rs、sea-query backend/语句是一棵树、QueryBuilder 与参数
4. DatabaseConnection::query_one_raw 按 inner 分派database/db_connection.rsDatabaseConnection 与 ConnectOptions
5. sqlx_query = query_with(AssertSqlSafe(sql), SqlxValues(values))driver/sqlx_postgres.rs、sea-query-sqlx绑到 sqlx
6. pool.acquire(),每条语句一次driver/sqlx_postgres.rsDatabaseConnection 与 ConnectOptions、sqlx 卷获取连接
7. fetch_one(&mut *conn):语句缓存、Parse / Bind / Execute;RowNotFound → Ok(None)sqlxsqlx 卷一次查询的全过程、错误
8. PgRow → QueryResult → SelectModel → Model::from_query_resultdriver/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_connections 2,于是新建了一条(进程 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 生成。

💬 关联讨论

发起讨论
加载中...