DatabaseConnection 与 ConnectOptions
主线位置:三条主线共同的落点。查询主线里的
db.query_one(..)、写入主线里的db.execute(..)/query_one(..),db都是这里讲的DatabaseConnection。本页讲它从哪来:Database::connect怎么选驱动,ConnectOptions的每一项变成 sqlx 的什么设置,以及ConnectionTrait上两组执行方法的区别。每条语句怎么借连接,见一次 find 的全过程;池内部怎么工作,见 sqlx 卷的连接池。
flowchart TB opts[ConnectOptions] --> dc["Database::connect"] dc -->|"scheme 是 postgres"| pgc["SqlxPostgresConnector::connect"] pgc --> copt["PgConnectOptions"] pgc --> popt["PoolOptions"] copt -->|"日志 / 应用名 / statement_timeout"| cw["connect_with"] popt -->|"超时 / 借出检查 / SET search_path"| cw cw --> pool[PgPool] pool --> dbc["DatabaseConnection"] dbc -->|"回调一次"| ac["after_connect 回调"]
Database::connect:按 scheme 选驱动
Database::connect 先用 url crate 解析一遍连接串,再按编译进来的驱动逐个问「这个 URL 是不是你的」:
pub async fn connect<C>(opt: C) -> Result<DatabaseConnection, DbErr>
where
C: Into<ConnectOptions>,
{
let opt: ConnectOptions = opt.into();
if url::Url::parse(&opt.url).is_err() {
return Err(conn_err(format!(
"The connection string '{}' cannot be parsed.",
opt.url
)));
}
// ...
#[cfg(feature = "sqlx-postgres")]
if DbBackend::Postgres.is_prefix_of(&opt.url) {
return crate::SqlxPostgresConnector::connect(opt).await;
}
// ...
Err(conn_err(format!(
"The connection string '{}' has no supporting driver.",
opt.url
)))
}
DbBackend::is_prefix_of 只看 scheme:Postgres 认 postgres 和 postgresql,MySQL 认 mysql,SQLite 认 sqlite。Into<ConnectOptions> 对所有 Into<String> 都有实现,所以 Database::connect("postgres://...") 直接传字符串也行,等于 ConnectOptions::new(url) 全用默认值。
只开了 sqlx-postgres 的程序里试三个连接串(src/bin/raw_api.rs 第一段,M2 Max、macOS、rustc 1.99.0、Postgres 16.14):
postgresql://lab:lab@192.168.64.106/lab => Postgres
mysql://lab:s3cret@db.internal/lab => Connection Error: The connection string 'mysql://lab:s3cret@db.internal/lab' has no supporting driver.
lab:s3cret@192.168.64.106/lab => Connection Error: The connection string 'lab:s3cret@192.168.64.106/lab' has no supporting driver.
DatabaseConnection::default() => Connection Error: Disconnected / Conn(Internal("Disconnected"))
第三个连接串少了 postgres://,url crate 把 lab 当成了 scheme,解析成功,于是落到「没有驱动」。两条错误都把整个连接串连同密码原样放进了错误文本,这类错误不该进日志或返回给用户,见错误。
DatabaseConnection:一个 struct,Postgres 分支里只有一个池
2.0 起 DatabaseConnection 是 struct,1.x 的那个 enum 搬进了 inner 字段,改名 DatabaseConnectionType:
#[derive(Debug, Clone)]
#[non_exhaustive]
pub struct DatabaseConnection {
/// Driver-specific connection or pool. Held in a field so we can attach
/// orthogonal state (e.g. RBAC) alongside.
pub inner: DatabaseConnectionType,
#[cfg(feature = "rbac")]
pub(crate) rbac: crate::RbacEngineMount,
}
#[derive(Clone)]
pub enum DatabaseConnectionType {
// ...
/// PostgreSQL connection pool (`sqlx-postgres`).
#[cfg(feature = "sqlx-postgres")]
SqlxPostgresPoolConnection(crate::SqlxPostgresPoolConnection),
// ...
/// In-memory mock connection used for testing (`mock`).
#[cfg(feature = "mock")]
MockDatabaseConnection(Arc<crate::MockDatabaseConnection>),
// ...
Disconnected,
}
改成 struct 的原因写在字段注释里,CHANGELOG 记在 2.0.0 的破坏性改动下(PR #2671,说明只有一句「This is necessary to attach context to DataConnection」):要在连接旁边挂上 RBAC 引擎这类和驱动无关的状态。1.x 里 match db { DatabaseConnection::SqlxPostgresPoolConnection(..) => .. } 的写法在 2.0 要改成 match db.inner { DatabaseConnectionType::.. }。
SqlxPostgresPoolConnection 里是一个 sqlx::PgPool、一个可选的指标回调和一个 record_stmt_in_spans 开关。PgPool 内部是 Arc,所以 DatabaseConnection::clone() 很便宜,克隆出来的共用同一个池,可以直接 move 进 spawn 出去的任务。DatabaseConnection::default() 是 Disconnected,在它上面执行任何语句都得到上面那条 Connection Error: Disconnected。哪些分支存在由 feature 决定,打开 mock 会多出 MockDatabaseConnection 分支,见 Mock 与 Proxy。
已有一个 sqlx 池时,From<PgPool> for DatabaseConnection 和 SqlxPostgresConnector::from_sqlx_postgres_pool 直接把它包起来。这两条路完全不经过 ConnectOptions:下面讲的 search_path、日志级别、借出检查都不会生效,池怎么配全看你自己的 PgPoolOptions。
SqlxPostgresConnector::connect:一项项翻译
ConnectOptions 的字段都是 pub(crate),只能通过 setter 设置。真正读它们的是 SqlxPostgresConnector::connect,按顺序做这几件事:
pub async fn connect(options: ConnectOptions) -> Result<DatabaseConnection, DbErr> {
let record_stmt_in_spans = options.get_record_stmt_in_spans();
let mut sqlx_opts = options
.url
.parse::<PgConnectOptions>()
.map_err(sqlx_error_to_conn_err)?;
use sqlx::ConnectOptions;
if !options.sqlx_logging {
sqlx_opts = sqlx_opts.disable_statement_logging();
} else {
sqlx_opts = sqlx_opts.log_statements(options.sqlx_logging_level);
if options.sqlx_slow_statements_logging_level != LevelFilter::Off {
sqlx_opts = sqlx_opts.log_slow_statements(
options.sqlx_slow_statements_logging_level,
options.sqlx_slow_statements_logging_threshold,
);
}
}
if let Some(application_name) = &options.application_name {
sqlx_opts = sqlx_opts.application_name(application_name);
}
if let Some(timeout) = options.statement_timeout {
sqlx_opts = sqlx_opts.options([("statement_timeout", timeout.as_millis().to_string())]);
}
if let Some(f) = &options.pg_opts_fn {
sqlx_opts = f(sqlx_opts);
}
let set_search_path_sql = // ... 见下文
// ...
let mut pool_options = options.sqlx_pool_options();
pool_options = crate::ConnectOptions::apply_before_acquire::<sqlx::Postgres>(
pool_options,
ping_after_idle,
pg_before_acquire,
);
if let Some(sql) = set_search_path_sql {
pool_options = pool_options.after_connect(move |conn, _| {
// ... 每条新连接执行一次 SET search_path
});
}
if let Some(f) = &pg_pool_opts_fn {
pool_options = f(pool_options);
}
let pool = if lazy {
pool_options.connect_lazy_with(sqlx_opts)
} else {
pool_options
.connect_with(sqlx_opts)
.await
.map_err(sqlx_error_to_conn_err)?
};
// ... 包成 DatabaseConnection
if let Some(cb) = after_connect {
cb(conn.clone()).await?;
}
Ok(conn)
}
两组设置分开走:日志、application_name、statement_timeout 写进 sqlx 的 PgConnectOptions(每条连接握手时用),池大小、超时、借出检查、SET search_path 写进 PoolOptions(池的行为)。两个 map_* 逃生口分别排在各自那一组的最后:map_sqlx_postgres_opts 能改掉前面设的应用名和 statement_timeout,map_sqlx_postgres_pool_opts 能覆盖池的一切,包括 sea-orm 刚装上的 after_connect(下文有实验)。
默认 connect_lazy 为 false,走 sqlx 的 connect_with:截止时间是「现在 + acquire_timeout」,在这之内借出一条连接再还回去,所以 Database::connect 成功就说明库能连上,池里留着一条空闲连接;connect_lazy(true) 走 connect_lazy_with,一条也不连,错误推迟到第一条语句(sqlx 卷的 PoolOptions 与默认值)。
对照表:每一项落到 sqlx 的哪里
ConnectOptions 方法 | sea-orm 默认 | 落到 sqlx 的哪里 | 不设时 sqlx 的值 |
|---|---|---|---|
max_connections | 不设 | PoolOptions::max_connections | 10 |
min_connections | 不设 | PoolOptions::min_connections | 0 |
connect_timeout | 不设 | PoolOptions::acquire_timeout | 30 秒 |
acquire_timeout | 不设 | PoolOptions::acquire_timeout(写在 connect_timeout 之后) | 30 秒 |
idle_timeout | 不设 | PoolOptions::idle_timeout,传 None 关掉 | 10 分钟 |
max_lifetime | 不设 | PoolOptions::max_lifetime,传 None 关掉 | 30 分钟 |
test_before_acquire | true | PoolOptions::test_before_acquire | true |
test_before_acquire_if_idle_for | 不设 | test_before_acquire(false) + 一个 before_acquire:空闲够久才 ping | — |
map_sqlx_postgres_before_acquire | 不设 | PoolOptions::before_acquire,排在上一项的 ping 之后 | 无 |
set_schema_search_path | 不设 | PoolOptions::after_connect:每条新连接执行一次 SET search_path = ... | 服务端默认 |
connect_lazy | false | connect_lazy_with / connect_with | — |
sqlx_logging | true | false 时 disable_statement_logging() | 开 |
sqlx_logging_level | INFO | PgConnectOptions::log_statements | DEBUG |
sqlx_slow_statements_logging_settings | (Off, 1s) | 只有级别不是 Off 时才调 log_slow_statements | WARN、1 秒 |
set_application_name | 不设 | PgConnectOptions::application_name | 不发 |
statement_timeout | 不设 | PgConnectOptions::options:启动消息里的 -c statement_timeout=毫秒 | 服务端默认 |
map_sqlx_postgres_opts | 不设 | 拿到 PgConnectOptions 随便改,在上面三项之后 | — |
map_sqlx_postgres_pool_opts | 不设 | 拿到 PoolOptions 随便改,最后执行 | — |
after_connect | 不设 | 不是 sqlx 的回调:池建好后用 DatabaseConnection 调一次 | — |
record_stmt_in_spans | true | 不是 sqlx 的设置:tracing-spans 的 span 里记不记 SQL | — |
前八行池参数的翻译在 ConnectOptions::sqlx_pool_options,sqlx 一侧的默认值在 PoolOptions::new。sea-orm 自己定下的默认值都在 ConnectOptions::new 里:开日志、日志级别 INFO、慢日志 (Off, 1s)、借出前 ping、不懒连接、span 里记 SQL;池的数字全部是「不设」,交给 sqlx。用 connect_lazy(true)(不连库)建池,再用 get_postgres_connection_pool().options() 打出 sqlx 实际拿到的 PoolOptions(src/bin/timeouts.rs 第一段,除了最后一行都设了 max_connections(1)):
connect_timeout=- acquire_timeout=- => PoolOptions { max_connections: 1, min_connections: 0, connect_timeout: 30s, max_lifetime: Some(1800s), idle_timeout: Some(600s), test_before_acquire: true }
connect_timeout=1000ms acquire_timeout=- => PoolOptions { max_connections: 1, min_connections: 0, connect_timeout: 1s, max_lifetime: Some(1800s), idle_timeout: Some(600s), test_before_acquire: true }
connect_timeout=- acquire_timeout=1000ms => PoolOptions { max_connections: 1, min_connections: 0, connect_timeout: 1s, max_lifetime: Some(1800s), idle_timeout: Some(600s), test_before_acquire: true }
connect_timeout=1500ms acquire_timeout=500ms => PoolOptions { max_connections: 1, min_connections: 0, connect_timeout: 500ms, max_lifetime: Some(1800s), idle_timeout: Some(600s), test_before_acquire: true }
connect_timeout=500ms acquire_timeout=1500ms => PoolOptions { max_connections: 1, min_connections: 0, connect_timeout: 1.5s, max_lifetime: Some(1800s), idle_timeout: Some(600s), test_before_acquire: true }
ConnectOptions::new 默认 => PoolOptions { max_connections: 10, min_connections: 0, connect_timeout: 30s, max_lifetime: Some(1800s), idle_timeout: Some(600s), test_before_acquire: true }
Debug 里那个 connect_timeout 是 sqlx 的旧名字,打印的其实是 acquire_timeout 字段(sqlx 卷 PoolOptions 与默认值里讲过)。后两行已经看得出结论:两个都设时,结果总是 acquire_timeout 的值,和谁大谁小无关。
connect_timeout 就是 acquire_timeout
原因就在 sqlx_pool_options 里这两段的先后:
if let Some(connect_timeout) = self.connect_timeout {
opt = opt.acquire_timeout(connect_timeout);
}
if let Some(idle_timeout) = self.idle_timeout {
opt = opt.idle_timeout(idle_timeout);
}
if let Some(acquire_timeout) = self.acquire_timeout {
opt = opt.acquire_timeout(acquire_timeout);
}
两个 setter 写的是 sqlx 的同一个字段,后写的赢。sqlx 的 Postgres 驱动没有单独的「TCP 连接超时」,连接超时归池管(sqlx 卷连接选项与建立连接),所以 sea-orm 的 connect_timeout 实际控制三件事:Database::connect 最多等多久、池满时一条语句排队最多等多久、池需要新建连接时最多连多久。
用同一组设置试两种连不上的库(src/bin/timeouts.rs 第二、三段,max_connections(1))。第二段连本机没人监听的 1 号端口,内核直接回 RST;第三段连程序里开的一个 TCP 监听,它接受连接但从不回话,sqlx 发出启动消息后就一直等:
for (c, a) in CASES {
let started = Instant::now();
let res = Database::connect(opts("postgres://lab:lab@127.0.0.1:1/lab", c, a)).await;
let err = res.err().expect("must fail");
println!("{} 用时 {:.2}s {err}", label(c, a), started.elapsed().as_secs_f64());
}
--- 2. Database::connect 到没人监听的端口 127.0.0.1:1 ---
connect_timeout=1000ms acquire_timeout=- 用时 1.00s Connection Error: pool timed out while waiting for an open connection
connect_timeout=- acquire_timeout=1000ms 用时 1.00s Connection Error: pool timed out while waiting for an open connection
connect_timeout=1500ms acquire_timeout=500ms 用时 0.50s Connection Error: pool timed out while waiting for an open connection
connect_timeout=500ms acquire_timeout=1500ms 用时 1.50s Connection Error: pool timed out while waiting for an open connection
--- 3. Database::connect 到只收不答的端口 ---
connect_timeout=1000ms acquire_timeout=- 用时 1.01s Conn(SqlxError(PoolTimedOut))
connect_timeout=- acquire_timeout=1000ms 用时 1.00s Conn(SqlxError(PoolTimedOut))
connect_timeout=1500ms acquire_timeout=500ms 用时 0.50s Conn(SqlxError(PoolTimedOut))
connect_timeout=500ms acquire_timeout=1500ms 用时 1.50s Conn(SqlxError(PoolTimedOut))
- 只设一个时,两者效果一样;两个都设时,用时总是
acquire_timeout。 - 连接被拒和对方不回话,最后都是 sqlx 的
PoolTimedOut:前者 sqlx 在截止时间内一直退避重试(sqlx 卷获取连接的「connect:重试和退避」),后者整个连接过程被截止时间截断。真正的原因(ConnectionRefused)不会出现在错误里。 - 建池时的超时被
sqlx_error_to_conn_err包成DbErr::Conn(SqlxError(PoolTimedOut)),Display是 sqlx 的原文pool timed out while waiting for an open connection。执行语句时借不到连接走的是另一个函数,错误长得不一样,见本页的动脑筋。 - 什么都不设时是 sqlx 的 30 秒:数据库宕机时,应用启动会卡 30 秒才报错。
src/bin/timeouts.rs 的第四段:max_connections(1),连上之后用 sqlx 的 pool.acquire() 把唯一一条连接占住,再在 sea-orm 上执行一条语句:
let db = Database::connect(opts(DIRECT, c, a)).await?;
let held = db.get_postgres_connection_pool().acquire().await.expect("hold");
let started = Instant::now();
let res = db.query_one_raw(Statement::from_string(DbBackend::Postgres, "select 1")).await;
let err = res.err().expect("must time out");
println!("{} 用时 {:.2}s {err}", label(c, a), started.elapsed().as_secs_f64());
connect_timeout(1500ms)、acquire_timeout(500ms) 时,这条语句失败前等了几秒?打印出来的错误文本和上面 Database::connect 超时的那句一样吗?
同一个程序换成 sea-orm 1.1.20(sqlx 0.8.6,crate exp-seaorm-conn1-old)跑,四段输出逐字相同:1.1.20 的 sqlx_pool_options 里也是这三段、这个顺序。
connect_timeout 这个名字是 sqlx 0.5 留下的sea-orm 0.9.0 之前,这一行是 opt = opt.connect_timeout(connect_timeout),名字和 sqlx 一一对应。sqlx 0.6.0 的 CHANGELOG 在 #1901 下写着「Renamed PoolOptions::connect_timeout to acquire_timeout for clarity.」sea-orm 在 PR #834(「Bump version of sqlx, sea-query, sea-schema, time and uuid」,首次发布于 0.9.0,说明里列着「Upgrade sqlx to ^0.6」)升级 sqlx 时只把调用改成了 opt.acquire_timeout(connect_timeout),自己的方法名没动。几个月后 issue #896 报告并发查询时 Failed to acquire connection from pool,PR #897(「Configure acquire timeout for connection pool」,首次发布于 0.10.0)加了 ConnectOptions::acquire_timeout,说明的「Adds」一节只有一行「Add acquire_timeout in ConnectOptions」,新的 if 写在 idle_timeout 之后。于是从 0.10.0 到 2.0.4,同一个 sqlx 字段有两个 setter,后写的那个赢。
日志:INFO 级别,慢语句日志关不掉
sea-orm 默认把每条语句的 sqlx 日志设成 INFO(sqlx 自己默认 DEBUG),所以 RUST_LOG=info 就会看到每条 SQL;不想要就 sqlx_logging(false) 或调 sqlx_logging_level。慢语句日志的处理有一个不对称:connect 里只有级别不是 Off 时才调 log_slow_statements。sea-orm 的默认值恰好是 Off,于是什么也不调,sqlx 自己的默认值(LogSettings::default:WARN、1 秒)原样生效。执行一条 select pg_sleep(1.1)(src/bin/slow_log.rs,订阅 sqlx=info):
=== 默认 ===
-- 第 1 条(上一条结束后又等了 200+0ms)
[conn 1] C->S Sync
[conn 1] S->C ReadyForQuery(I)
[conn 1] C->S Parse(sqlx_s_1, "select pg_backend_pid() as pid"), Describe(S, sqlx_s_1), Sync
...
-- 第 2 条(上一条结束后又等了 200+50ms)
[conn 1] C->S Sync
[conn 1] S->C ReadyForQuery(I)
[conn 1] C->S Bind(portal unnamed, stmt sqlx_s_1, ), Execute, Close(P, unnamed), Sync
...
=== test_before_acquire(false) ===
-- 第 1 条(上一条结束后又等了 200+0ms)
[conn 2] C->S Parse(sqlx_s_1, "select pg_backend_pid() as pid"), Describe(S, sqlx_s_1), Sync
...
-- 第 3 条(上一条结束后又等了 200+500ms)
[conn 2] C->S Bind(portal unnamed, stmt sqlx_s_1, ), Execute, Close(P, unnamed), Sync
...
=== test_before_acquire_if_idle_for(300ms) ===
...
-- 第 2 条(上一条结束后又等了 200+50ms)
[conn 3] C->S Bind(portal unnamed, stmt sqlx_s_1, ), Execute, Close(P, unnamed), Sync
...
-- 第 3 条(上一条结束后又等了 200+500ms)
[conn 3] C->S Sync
[conn 3] S->C ReadyForQuery(I)
[conn 3] C->S Bind(portal unnamed, stmt sqlx_s_1, ), Execute, Close(P, unnamed), Sync
...
=== before_acquire:age 超过 400ms 就丢 ===
-- 第 1 条(上一条结束后又等了 200+0ms)
before_acquire: age=204.712459ms idle_for=204.709875ms -> true
[conn 4] C->S Parse(sqlx_s_1, "select pg_backend_pid() as pid"), Describe(S, sqlx_s_1), Sync
...
pg_backend_pid = 1932
-- 第 2 条(上一条结束后又等了 200+50ms)
before_acquire: age=463.819959ms idle_for=256.242875ms -> false
[conn 4] C->S Terminate
[conn 5] C->S StartupMessage(v3.0)
...
pg_backend_pid = 1933
getter 说 Off,超过 1 秒的语句照样打 WARN;显式传 (Off, ..) 也关不掉。只有 sqlx_logging(false) 走 disable_statement_logging(),把普通日志和慢日志一起关掉。1.1.20 的这段代码相同,跑出来也一样。
会话设置:search_path、statement_timeout、application_name
每条语句可能落在池里任何一条连接上(一次 find 的全过程),所以会话级的设置必须在每条连接建立时设好,在 &DatabaseConnection 上执行一次 SET 没用。ConnectOptions 用了两种办法。
set_schema_search_path:拼一条 SET,装进 after_connect
let set_search_path_sql = options.schema_search_path.as_ref().map(|schema| {
let mut string = "SET search_path = ".to_owned();
if schema.starts_with('"') {
write!(&mut string, "{schema}").expect("Infallible");
} else {
for (i, schema) in schema.split(',').enumerate() {
if i > 0 {
write!(&mut string, ",").expect("Infallible");
}
if schema.starts_with('"') {
write!(&mut string, "{schema}").expect("Infallible");
} else {
write!(&mut string, "\"{schema}\"").expect("Infallible");
}
}
}
string
});
规则只有两条:整个值以 " 开头就原样拼上;否则按逗号切开,每段不以 " 开头的就在两边加一对双引号。没有去空格,也没有转义值里的 "。这条 SQL 装进 sqlx 的 after_connect,用 sqlx::Executor::execute(conn, sqlx::AssertSqlSafe(sql)) 执行。不带参数的字符串在 sqlx 里走简单查询协议(sqlx 卷扩展查询协议与语句缓存的「简单查询的分支」),一条 Query 消息里可以有多条语句。
五种写法各连一次,打开 sqlx 日志,连上后查 current_setting('search_path')(src/bin/session_opts.rs 第一、二段,connect_timeout 1 秒):
## set_schema_search_path("so_conn1")
INFO sqlx::query: summary="SET search_path = \"so_conn1\"" db.statement="" rows_affected=0 rows_returned=0 elapsed=543.334µs elapsed_secs=0.000543334
search_path = so_conn1
## set_schema_search_path("so_conn1,public")
INFO sqlx::query: summary="SET search_path = \"so_conn1\",\"public\"" db.statement="" rows_affected=0 rows_returned=0 elapsed=300.167µs elapsed_secs=0.000300167
search_path = so_conn1, public
## set_schema_search_path("so_conn1, public")
INFO sqlx::query: summary="SET search_path = \"so_conn1\",\" …" db.statement="\n\nSET search_path = \"so_conn1\",\" public\"\n" rows_affected=0 rows_returned=0 elapsed=245.708µs elapsed_secs=0.000245708
search_path = so_conn1, " public"
## set_schema_search_path("\"so_conn1\"")
INFO sqlx::query: summary="SET search_path = \"so_conn1\"" db.statement="" rows_affected=0 rows_returned=0 elapsed=317.5µs elapsed_secs=0.0003175
search_path = so_conn1
## set_schema_search_path("my\"schema")
INFO sqlx::query: summary="SET search_path = \"my\"schema\"" db.statement="" rows_affected=0 rows_returned=0 elapsed=1.061042ms elapsed_secs=0.001061042
ERROR sqlx_core::pool::inner: error returned from after_connect error=error returned from database: syntax error at or near "schema" at line 1244
INFO sqlx::query: summary="SET search_path = \"my\"schema\"" db.statement="" rows_affected=0 rows_returned=0 elapsed=559.583µs elapsed_secs=0.000559583
ERROR sqlx_core::pool::inner: error returned from after_connect error=error returned from database: syntax error at or near "schema" at line 1244
...
connect 失败,用时 1.00s:Connection Error: pool timed out while waiting for an open connection
--- 2. 以引号开头的值原样拼进 SQL ---
INFO sqlx::query: summary="SET search_path = \"so_conn1\"; …" db.statement="\n\nSET search_path = \"so_conn1\"; SET application_name = 'injected'\n" rows_affected=0 rows_returned=0 elapsed=324.667µs elapsed_secs=0.000324667
search_path = so_conn1
application_name = injected
"so_conn1, public":逗号后面的空格被包进了引号,第二个 schema 叫public(带前导空格),不存在,Postgres 不报错,静默跳过。要么不写空格,要么整个值自己加好引号再传。- 值里有
":拼出SET search_path = "my"schema",语法错误。同样的一对INFO/ERROR在这一秒里一共出现 8 次(中间几次省略了)。after_connect出错时 sqlx 记一条ERROR日志、扔掉这条连接、退避后重连(sqlx 卷获取连接),直到截止时间,最后Database::connect报的是pool timed out,语法错误只出现在日志里。at line 1244是 Postgres 服务端 C 源码的行号(sqlx 卷错误的「0.9 的「at line N」」)。 - 以
"开头的值不做任何处理,又走简单查询协议,分号后面的语句也会在每条新连接上执行一遍。set_schema_search_path只能收配置文件里写死的值,不能收任何外部输入。
1.1.3 之前,这段代码直接把值拼在 SET search_path = 后面,不加引号。PR #2436(「Put schema in quotes for postgres driver」,首次发布于 1.1.3)的说明:「Previously if the schema contained special characters like a -, the pool connection would end up timing out because of a syntax error. This PR fixes that syntax error.」PR 里附的修复前测试日志,和上面 my"schema 那一段是同一个样子:SET search_path = schema-with-special-characters 之后一行 ERROR sqlx_core::pool::inner: error returned from after_connect error=error returned from database: syntax error at or near "-",重复十几次,最后连接池超时。修复的办法是加引号,对已经以 " 开头的值保持原样(兼容自己加了引号的用户),但没有转义值内部的 ",所以同样的超时在值里带引号时还会出现。
statement_timeout 和 application_name:写进启动消息
这两项不发 SET,而是写进 PgConnectOptions,在握手的 StartupMessage 里一起发给服务端,不多一个来回。statement_timeout 用的是 sqlx 的 PgConnectOptions::options,它往 options 参数里追加一段 -c key=value,所以和 URL 里已有的 options 并存。URL 里带 ?options=-c%20search_path%3Dso_conn1,再设 statement_timeout(300ms) 和 set_application_name("conn1-app")(session_opts 第三段):
PgConnectOptions.options = Some("-c search_path=so_conn1 -c statement_timeout=300")
PgConnectOptions.application_name = Some("conn1-app")
statement_timeout = 300ms
search_path = so_conn1
application_name = conn1-app
INFO sqlx::query: summary="select pg_sleep(1)" db.statement="" rows_affected=0 rows_returned=0 elapsed=307.602208ms elapsed_secs=0.307602208
pg_sleep(1):Execution Error: error returned from database: canceling statement due to statement timeout at line 3394
statement_timeout 的单位是毫秒(timeout.as_millis()),超时的语句由服务端取消,sea-orm 收到的是普通的数据库错误(这里是 execute_raw,所以包成 Exec)。URL 里的 options 参数也是设 search_path 的另一条路:它走 sqlx 的转义(PgOptionsWriteEscaped),不经过 SQL,不存在上面那几种拼接问题。
after_connect 回调:每个池一次,不是每条连接一次
ConnectOptions::after_connect 的文档写的是「Set a callback function that will be called after a new connection is established」,但它和 sqlx 的 after_connect 不是一回事:connect 的最后一步 cb(conn.clone()).await?,传进去的是整个 DatabaseConnection,只调一次。max_connections(3),回调里计数,再用 sqlx 的 pool.acquire() 把三条连接都建出来(session_opts 第四段);然后在 map_sqlx_postgres_pool_opts 里装一个什么也不做的 sqlx after_connect,同时设 set_schema_search_path(第五段):
--- 4. ConnectOptions::after_connect 被调用几次 ---
池里 3 条连接,after_connect 调用了 1 次
--- 5. map_sqlx_postgres_pool_opts 里再设 after_connect ---
INFO sqlx::query: summary="select current_setting('search_path') as v" db.statement="" rows_affected=1 rows_returned=1 elapsed=1.140042ms elapsed_secs=0.001140042
search_path = "$user", public
第五段没有 SET search_path 那行日志:sqlx 的 PoolOptions::after_connect 是替换不是叠加,map_sqlx_postgres_pool_opts 又排在最后,sea-orm 装的那个就被换掉了。
问: 我要给每条连接设 SET SESSION ...,ConnectOptions::after_connect 名字正合适,为什么不能用?
答: 它是一个「连接池建好之后」的钩子,回调里拿到的是 DatabaseConnection,在上面执行的 SET 只落在当时借到的那一条连接上,之后新建的连接都没有。这个回调来自 issue #2708,原本的诉求正是像 sqlx 的 .after_connect 那样给 MySQL 设 sql_mode;PR #2768(仓库里最早含它的 tag 是 2.0.0-rc.28,更早的 rc 没有打 tag)实现成了现在这样。要每条连接都执行,用 map_sqlx_postgres_pool_opts(|o| o.after_connect(..)) 装 sqlx 的回调,但它会顶掉 set_schema_search_path,两件事要在同一个回调里一起做。只是设参数的话,更简单的是 URL 里的 options=-c name=value,或者 map_sqlx_postgres_opts(|o| o.options([..])),它们在握手时就生效。
借出前的检查:每次 ping,还是空闲够久才 ping
test_before_acquire 默认 true,sqlx 每次从空闲队列借出连接都先 ping 一次(一个 Sync 换一个 ReadyForQuery),多一个来回。2.0.1 加了 test_before_acquire_if_idle_for 和 map_sqlx_postgres_before_acquire(PR #3143),两者由 apply_before_acquire 合成 sqlx 唯一的那个 before_acquire:
if ping_after_idle.is_none() && user_cb.is_none() {
return opt;
}
if ping_after_idle.is_some() {
opt = opt.test_before_acquire(false);
}
opt.before_acquire(move |conn, meta| {
let user_cb = user_cb.clone();
Box::pin(async move {
if let Some(threshold) = ping_after_idle {
// ...
if meta.idle_for >= threshold {
conn.ping().await?;
}
}
match user_cb {
Some(user_cb) => user_cb(conn, meta).await,
None => Ok(true),
}
})
})
设了阈值就强制关掉每次都 ping(即使之后又调了 test_before_acquire(true)),改成 idle_for 达到阈值才 ping;用户回调排在 ping 之后,返回 Ok(false) 就丢掉这条连接。经代理连库,池里 1 条连接,连续三条语句:建池后等 200 毫秒发第 1 条,之后每条语句结束先等 200 毫秒,再分别等 50、500 毫秒发第 2、3 条(src/bin/idle_ping.rs;四种设置各建一个池,输出很长,只留每条语句开头的几行):
=== 默认 ===
-- 第 1 条
[conn 1] C->S Sync
[conn 1] S->C ReadyForQuery(I)
[conn 1] C->S Parse(sqlx_s_1, "select pg_backend_pid() as pid"), Describe(S, sqlx_s_1), Sync
...
=== test_before_acquire(false) ===
-- 第 1 条
[conn 2] C->S Parse(sqlx_s_1, "select pg_backend_pid() as pid"), Describe(S, sqlx_s_1), Sync
...
=== test_before_acquire_if_idle_for(300ms) ===
-- 第 2 条(上一条结束后又等了 200+50ms)
[conn 3] C->S Bind(portal unnamed, stmt sqlx_s_1, ), Execute, Close(P, unnamed), Sync
...
-- 第 3 条(上一条结束后又等了 200+500ms)
[conn 3] C->S Sync
[conn 3] S->C ReadyForQuery(I)
[conn 3] C->S Bind(portal unnamed, stmt sqlx_s_1, ), Execute, Close(P, unnamed), Sync
...
=== before_acquire:age 超过 400ms 就丢 ===
-- 第 2 条(上一条结束后又等了 200+50ms)
before_acquire: age=460.341875ms idle_for=254.167ms -> false
[conn 4] C->S Terminate
[conn 5] C->S StartupMessage(v3.0)
...
pg_backend_pid = 1923
- 默认:三条语句前面都有一对
Sync/ReadyForQuery。 test_before_acquire(false):三条都没有。- 阈值 300 毫秒:只有空闲了约 700 毫秒的第 3 条前面有 ping。
- 回调返回
false:sqlx 发Terminate关掉旧连接,当场新建一条(完整的握手),后端进程号从 1932 变成 1933。新建的连接不经过before_acquire。
每条语句之后那对 Sync / ReadyForQuery 在四种设置下都在,那是归还时的检查,和这几个选项无关(sqlx 卷归还、Drop 与维护任务)。
ConnectionTrait:语句树、*_raw 和 execute_unprepared
ConnectionTrait 的执行方法分三组,每种连接必须实现的是 get_database_backend 和后两组里的四个,第一组是默认方法:
| 方法 | 收什么 | 实现 |
|---|---|---|
execute / query_one / query_all | &S: StatementBuilder(sea-query 语句树) | 默认方法:get_database_backend().build(stmt) 再调对应的 *_raw |
execute_raw / query_one_raw / query_all_raw | Statement(SQL + 可选的 Values) | 每种连接自己实现 |
execute_unprepared | &str | 每种连接自己实现 |
收语句树的那组在 2.0 才有,来历见一次 find 的全过程的冷知识(PR #2657)。到了 Postgres 驱动,execute_raw / query_*_raw 都经 sqlx_query 变成 sqlx::query_with(AssertSqlSafe(sql), SqlxValues(values)),即使 Statement 没有值,也传一个空的 Values;而 execute_unprepared 是 conn.execute(sqlx::AssertSqlSafe(sql.to_owned())),不带参数。经代理看四种调用发了什么(src/bin/raw_api.rs 第二段):
let q = Query::select().expr(Expr::val(7).add(1)).to_owned();
db.execute(&q).await?;
let stmt = Statement::from_sql_and_values(DbBackend::Postgres, "select $1::int + 1", [7.into()]);
db.execute_raw(stmt).await?;
let stmt = Statement::from_string(DbBackend::Postgres, "select 1; select 2");
db.execute_raw(stmt).await.err().expect("two statements");
db.execute_unprepared("select 1; select 2").await?;
## execute(&SelectStatement)
[conn 1] C->S Parse(sqlx_s_1, "SELECT $1 + $2"), Describe(S, sqlx_s_1), Sync
[conn 1] S->C ParseComplete, ParameterDescription(OID 23/23), RowDescription, ReadyForQuery(I)
[conn 1] C->S Bind(portal unnamed, stmt sqlx_s_1, $1=0x00000007 $2=0x00000001), Execute, Close(P, unnamed), Sync
[conn 1] S->C BindComplete, DataRow(列长 4), CommandComplete("SELECT 1"), CloseComplete, ReadyForQuery(I)
INFO sqlx::query: summary="SELECT $1 + $2" db.statement="" rows_affected=1 rows_returned=1 elapsed=3.024875ms elapsed_secs=0.003024875
[conn 1] C->S Sync
[conn 1] S->C ReadyForQuery(I)
rows_affected = 1
## execute_raw(Statement::from_sql_and_values)
[conn 1] C->S Parse(sqlx_s_2, "select $1::int + 1"), Describe(S, sqlx_s_2), Sync
[conn 1] S->C ParseComplete, ParameterDescription(OID 23), RowDescription, ReadyForQuery(I)
[conn 1] C->S Bind(portal unnamed, stmt sqlx_s_2, $1=0x00000007), Execute, Close(P, unnamed), Sync
[conn 1] S->C BindComplete, DataRow(列长 4), CommandComplete("SELECT 1"), CloseComplete, ReadyForQuery(I)
INFO sqlx::query: summary="select $1::int + 1" db.statement="" rows_affected=1 rows_returned=1 elapsed=1.835625ms elapsed_secs=0.001835625
[conn 1] C->S Sync
[conn 1] S->C ReadyForQuery(I)
rows_affected = 1
## execute_raw(Statement::from_string(两条语句))
[conn 1] C->S Parse(sqlx_s_3, "select 1; select 2"), Describe(S, sqlx_s_3), Sync
[conn 1] S->C ErrorResponse, ReadyForQuery(I)
INFO sqlx::query: summary="select 1; select 2" db.statement="" rows_affected=0 rows_returned=0 elapsed=1.351542ms elapsed_secs=0.001351542
[conn 1] C->S Sync
[conn 1] S->C ReadyForQuery(I)
Execution Error: error returned from database: cannot insert multiple commands into a prepared statement at line 1478
## execute_unprepared(两条语句)
[conn 1] C->S Query("select 1; select 2")
[conn 1] S->C RowDescription, DataRow(列长 1), CommandComplete("SELECT 1"), RowDescription, DataRow(列长 1), CommandComplete("SELECT 1"), ReadyForQuery(I)
INFO sqlx::query: summary="select 1; select 2" db.statement="" rows_affected=2 rows_returned=2 elapsed=1.112833ms elapsed_secs=0.001112833
[conn 1] C->S Sync
[conn 1] S->C ReadyForQuery(I)
rows_affected = 2
这个池设了 test_before_acquire(false),所以语句之前没有 ping;每段末尾那对 Sync / ReadyForQuery 是归还时的检查。
- 前三种都走扩展查询协议:
Parse进 sqlx 的语句缓存(sqlx_s_1、sqlx_s_2),值在Bind里。Statement::from_string没有值,也一样被预备,所以只能有一条语句。 execute_unprepared发一条简单查询Query:不预备、不进缓存、不能带参数,可以一次发多条语句,rows_affected是各条之和。适合迁移脚本、建表这种整段 SQL;值必须内联进字符串,所以不能拼用户输入。它的 tracing span 固定record_stmt = false,不记 SQL 原文。- 四种调用都是先
pool.acquire()借一条连接,语句结束就还。execute_unprepared里的SET同样只影响借到的那一条。
ConnectionTrait 也由 DatabaseTransaction 实现,所以同一个函数既能收 &db 也能收 &txn,见事务。
get_postgres_connection_pool:退回 sqlx
sea-orm 没包装的 sqlx 功能(LISTEN / NOTIFY、COPY、池的统计)可以通过 get_postgres_connection_pool 拿到 &sqlx::PgPool 直接用。它不是 Option:inner 不是 Postgres 分支时直接 panic!("Not Postgres Connection"),在可能是 mock 连接的代码里先 get_database_backend() 判断。raw_api 第三段:
let pool = db.get_postgres_connection_pool();
let n: i64 = sqlx::query_scalar("select 40::int8 + 2").fetch_one(pool).await?;
let from_pool: DatabaseConnection = pool.clone().into();
INFO sqlx::query: summary="select 40::int8 + 2" db.statement="" rows_affected=1 rows_returned=1 elapsed=2.680792ms elapsed_secs=0.002680792
sqlx::query_scalar(..).fetch_one(pool) = 42,池里 1 条
From<PgPool>:Postgres
直接用 sqlx 时,ConnectOptions 里设在 PgConnectOptions 和 PoolOptions 上的东西照样生效(这条语句也打了 INFO 日志),因为它们本来就属于这个池;而 sea-orm 自己那一层的东西(指标回调、tracing span)不会出现。listen / copy_in_raw 的用法见 sqlx 卷的 LISTEN 与 COPY。
和 1.1.20 的差别
| 项 | 1.1.20 | 2.0.4 |
|---|---|---|
DatabaseConnection | enum | struct,enum 在 inner 里(#2671) |
connect_timeout / acquire_timeout | 都映射到 sqlx acquire_timeout,后者赢 | 相同(实验输出逐字相同) |
idle_timeout / max_lifetime | 只收 Duration,关不掉 sqlx 的默认值 | 收 Into<Option<Duration>>,None 关掉(PR #2748) |
set_application_name、statement_timeout | 无 | 有(CHANGELOG 分别记在 2.0.0-rc.32、2.0.0-rc.37) |
map_sqlx_postgres_pool_opts、after_connect | 无 | 有(rc.38 的 #2770;#2768) |
test_before_acquire_if_idle_for、map_sqlx_postgres_before_acquire | 无 | 2.0.1 起(#3143) |
set_schema_search_path 的拼接、慢日志的处理 | 同一段代码 | 同左 |
| 执行方法 | 只有收 Statement 的一组 | 收语句树的一组 + *_raw(#2657) |
动脑筋答案
--- 4. 池满(max_connections=1,唯一一条被占住)时执行一条语句 ---
connect_timeout=1000ms acquire_timeout=- 用时 1.00s Failed to acquire connection from pool: Connection pool timed out
connect_timeout=- acquire_timeout=1000ms 用时 1.00s Failed to acquire connection from pool: Connection pool timed out
connect_timeout=1500ms acquire_timeout=500ms 用时 0.50s Failed to acquire connection from pool: Connection pool timed out
connect_timeout=500ms acquire_timeout=1500ms 用时 1.50s Failed to acquire connection from pool: Connection pool timed out
0.5 秒,和建池时一样是 acquire_timeout 赢:池只有一个超时字段,排队等连接和新建连接用的是同一个。错误文本不一样。执行语句时的 pool.acquire() 经 sqlx_conn_acquire_err 转换,PoolTimedOut 变成 sea-orm 自己的 DbErr::ConnectionAcquire(ConnAcquireErr::Timeout),Display 是 Failed to acquire connection from pool: Connection pool timed out;建池时的 connect_with 用的是 sqlx_error_to_conn_err,得到 DbErr::Conn(SqlxError(PoolTimedOut)),显示的是 sqlx 的原文。同一个 sqlx 错误,在 sea-orm 里有两种形状,按错误类型匹配时两种都要处理。1.1.20 跑出来的四行逐字相同。
小结
Database::connect先用url解析,再按 scheme(postgres/postgresql)选驱动;解析失败和没有驱动的错误文本都带着完整的连接串,包括密码。DatabaseConnection在 2.0 是 struct,inner里的 Postgres 分支只有一个PgPool,克隆共享同一个池;From<PgPool>包装现成的池时完全绕过ConnectOptions。- 池的数字全部是「不设就用 sqlx 的」:10 条、30 秒、空闲 10 分钟、寿命 30 分钟;sea-orm 自己只定了 INFO 日志、借出前 ping、不懒连接。
connect_timeout和acquire_timeout写的是 sqlx 的同一个acquire_timeout,后写的acquire_timeout赢;它同时管建池、排队和新建连接,连不上库最后只看到PoolTimedOut。- 慢语句日志的默认
Off不会传给 sqlx,超过 1 秒的语句照样打 WARN;只有sqlx_logging(false)能关。 set_schema_search_path是在每条新连接上执行一条拼出来的SET:逗号后的空格进了 schema 名,值里的"让建池超时,以"开头的值原样执行;statement_timeout、application_name和 URL 的options写进启动消息,更稳。ConnectOptions::after_connect每个池只调一次;sqlx 的after_connect要从map_sqlx_postgres_pool_opts装,会顶掉 search_path 的那个。test_before_acquire_if_idle_for只在连接空闲够久时 ping;before_acquire回调返回false时 sqlx 当场换一条新连接。execute/query_*收语句树,*_raw收Statement,都走扩展协议、只能一条语句;execute_unprepared走简单查询,能发多条、不能带参数。