Skip to content

Rust 连接 PostgreSQL ​

Rust 生态连接 PostgreSQL 的三大主角:tokio-postgres(异步原生驱动,API 贴近 libpq,控制力最强)、sqlx(异步全家桶,以编译期 SQL 校验独树一帜)、deadpool-postgres(基于 tokio-postgres 的成熟连接池)。本页按「生态选型 → tokio-postgres 基础与 COPY → sqlx 池与编译期校验 → deadpool 池 → JSONB 与错误处理」推进,最后落到 sqlx 连接池 + 事务的完整实战。

Rust 驱动层的两个核心观念与别的语言不同:所有权参与连接管理——事务对象借用客户端(&mut client),借用检查器从语言层面杜绝「事务中途复用连接」这类其他语言的经典事故;编译期把该做的事做完——sqlx 的 query! 宏在 cargo build 时连接数据库校验 SQL 的列名与类型,SQL 拼写错误、列改名导致的运行时崩溃在编译阶段就被拦截。

异步运行时统一采用 tokio(两套驱动的事实标准);代码基于 tokio-postgres 0.7 与 sqlx 0.7/0.8,API 细节以 docs.rs 各 crate 文档为准。

前置知识

建议先阅读:Node.js 连接 PostgreSQL

基础概念 ​

方案定位特点
tokio-postgres异步原生驱动libpq 风格 API、copy_in/copy_out、NOTIFY
sqlx异步驱动 + 编译期校验 + 轻量 ORMquery! 宏、离线模式、内建池
deadpool-postgres连接池基于 tokio-postgres,回收策略可配
tokio异步运行时连接推进任务、超时与取消的载体
text
  ┌───────────────┐
  │  axum/actix   │──► deadpool-postgres ──► tokio-postgres ──► PostgreSQL
  │  (Web 服务)    │
  └───────────────┘
  ┌───────────────┐
  │  业务服务       │──► sqlx::PgPool(自带池 + 编译期校验)──► PostgreSQL
  └───────────────┘

选型速览:

  • 要极致控制与 COPY/NOTIFY 等原生能力 → tokio-postgres + deadpool-postgres;
  • 要编译期 SQL 校验、内建池、ORM-lite 体验 → sqlx(两者可以共存:核心链路 sqlx,特殊通道 tokio-postgres);
  • 两者的参数占位符都是 $1,Row 取值都是「编译期泛型推断 + 运行时类型校验」。

Rust 特有的两条心智模型:

  1. 连接的推进任务:tokio_postgres::connect 返回 (Client, Connection) 二元组,Connection 必须交给独立任务 await(它就是网络读写循环)——忘了 spawn 它,所有查询都会挂起;
  2. 事务借用客户端:client.transaction() 返回的 Transaction 可变借用 client,事务期间无法用同一连接跑别的查询,借用结束(commit/rollback/drop)后归还——这是类型系统在替你把关事务边界。

语法与用法 ​

tokio-postgres 最小可用程序:

rust
use tokio_postgres::NoTls;

#[tokio::main]
async fn main() -> Result<(), tokio_postgres::Error> {
    let (client, connection) = tokio_postgres::connect(
        "host=db1 user=app_rw password=secret dbname=appdb sslmode=require",
        NoTls,
    )
    .await?;

    // Connection 必须在独立任务里推进,否则查询永远挂起
    tokio::spawn(async move {
        if let Err(e) = connection.await {
            eprintln!("connection error: {e}");
        }
    });

    let rows = client
        .query("SELECT id, name FROM users WHERE status = $1", &[&"active"])
        .await?;

    for row in rows {
        let id: i64 = row.get(0);            // 按下标
        let name: &str = row.get("name");    // 按列名
        println!("{id}: {name}");
    }
    Ok(())
}

sqlx 连接池:

rust
use sqlx::postgres::PgPoolOptions;

let pool = PgPoolOptions::new()
    .max_connections(20)
    .acquire_timeout(std::time::Duration::from_secs(3))
    .connect("postgres://app_rw:secret@db1:5432/appdb")
    .await?;

let user: Option<(i64, String)> = sqlx::query_as(
    "SELECT id, name FROM users WHERE id = $1",
)
.bind(42)
.fetch_optional(&pool)
.await?;

事务(sqlx):

rust
let mut tx = pool.begin().await?;
sqlx::query("UPDATE accounts SET balance = balance - $1 WHERE id = $2")
    .bind(100i64).bind(from_id)
    .execute(&mut *tx)
    .await?;
sqlx::query("UPDATE accounts SET balance = balance + $1 WHERE id = $2")
    .bind(100i64).bind(to_id)
    .execute(&mut *tx)
    .await?;
tx.commit().await?;   // 不 commit(含提前 return/panic)则 drop 时回滚

COPY(tokio-postgres):

rust
use futures::SinkExt;

let mut sink = client
    .copy_in("COPY stg_items FROM STDIN WITH (FORMAT csv, HEADER true)")
    .await?;
sink.send(bytes.as_slice()).await?;
let copied: u64 = sink.finish().await?;

详细说明 ​

生态地图:三件套如何组合 ​

tokio-postgres 是「底层引擎」:连接串解析、协议实现、COPY、NOTIFY 都在这里。sqlx 是「自成一体的上层」:它自带 PostgreSQL 协议实现(不依赖 tokio-postgres),外加连接池、查询宏与 FromRow 映射。deadpool-postgres 是「tokio-postgres 的池化外壳」。

text
  方案 A:deadpool-postgres ──► tokio-postgres ──► PostgreSQL
  方案 B:sqlx::PgPool ──────────────────────────► PostgreSQL(独立实现)

工程上的常见组合:Web 服务用 sqlx 一站式(池、宏校验、事务);数据管道/ETL 用 tokio-postgres(copy_in/copy_out 更顺);大型服务混用(sqlx 管业务查询,tokio-postgres 管特殊通道)。注意混用时两套错误类型、两套池各自独立管理。

tokio-postgres:连接、NoTls 与连接串 ​

tokio_postgres::connect 接受 libpq 风格连接串,TLS 用 NoTls 起步(需要证书校验时用 tokio-postgres-rustls 等社区 TLS 实现,以官方仓库说明为准):

rust
let (client, connection) = tokio_postgres::connect(
    "host=db1 port=5432 dbname=appdb user=app_rw password=secret \
     sslmode=require connect_timeout=5 application_name=svc-orders",
    NoTls,
).await?;

tokio::spawn(async move {
    if let Err(e) = connection.await {
        eprintln!("connection error: {e}");
    }
});

关键细节再强调一次:connection 必须被 await。它承担全部网络 I/O,通常 tokio::spawn 进后台任务并在错误时打日志。单连接 Client 的查询串行执行——并发查询请上池(deadpool)或多连接。

tokio-postgres:query/query_one/execute 与 Row 取值 ​

rust
// query:返回 Vec<Row>
let rows = client.query("SELECT id, name FROM users WHERE age > $1", &[&18]).await?;

// query_one:恰好一行,否则报错
let row = client.query_one("SELECT id, name FROM users WHERE id = $1", &[&42]).await?;

// query_opt:0 或 1 行,返回 Option<Row>
let maybe = client.query_opt("SELECT id FROM users WHERE email = $1", &[&email]).await?;

// execute:返回受影响行数
let n = client.execute("DELETE FROM sessions WHERE expired_at < now()", &[]).await?;
println!("deleted {n} rows");

Row::get 的类型由调用侧标注决定,运行时校验类型匹配(不匹配 panic):

rust
let id: i64 = row.get(0);                        // bigint
let name: String = row.get("name");              // text(取所有权)
let name: &str = row.get("name");                // text(借用,行还在时有效)
let amount: rust_decimal::Decimal = row.get(2);  // numeric(配 decimal crate 适配)
let ts: chrono::DateTime<chrono::Utc> = row.get(3); // timestamptz(配 chrono 特性)

i64 对应 int8、f64 对应 float8、bool 对应 bool 是内建映射;numeric/timestamp/uuid/jsonb 等需要启用对应 feature(chrono/uuid/serde_json 等)或使用类型适配 crate。取值类型与列类型不符会在运行时 panic——编译期校验的需求正是 sqlx 宏的价值所在。

tokio-postgres:事务 transaction() ​

rust
// transaction() 可变借用 client,事务期间独占
let tx = client.transaction().await?;

tx.execute("UPDATE accounts SET balance = balance - $1 WHERE id = $2",
           &[&100, &from]).await?;
tx.execute("UPDATE accounts SET balance = balance + $1 WHERE id = $2",
           &[&100, &to]).await?;

tx.commit().await?;
// 或 tx.rollback().await?;
// 两者都没调时:Transaction drop 自动触发回滚

savepoint 同样是事务对象上的方法(tx.savepoint()),嵌套事务通过在新事务对象上继续操作表达。借用模型的含义:编译期就不可能出现「事务未收尾就复用连接」——这把其他语言靠纪律保证的事,变成了类型系统的保证。

tokio-postgres:copy_in / copy_out 批量通道 ​

copy_in 返回 CopyInSink,实现 Sink<&[u8]>(需要 futures 的 SinkExt 提供 send),另有 finish() 收尾并返回拷贝行数:

rust
use futures::SinkExt;

// 从内存/文件逐块发送
let mut sink = client
    .copy_in("COPY stg_events (event_id, user_id, kind, event_time) \
              FROM STDIN WITH (FORMAT csv, HEADER true)")
    .await?;

for chunk in csv_chunks {          // BytesMut / Vec<u8> 均可
    sink.send(chunk.as_slice()).await?;
}
let copied = sink.finish().await?;
println!("copied {copied} rows");

CopyInSink 还提供 writer() 把自身转成 AsyncWrite,可以无缝接 tokio::io::copy 与文件流(签名细节以 docs.rs 为准):

rust
let mut writer = client
    .copy_in("COPY stg_items FROM STDIN WITH (FORMAT csv, HEADER true)")
    .await?
    .writer();
let mut file = tokio::fs::File::open("items.csv").await?;
tokio::io::copy(&mut file, &mut writer).await?;

copy_out 反向导出,实现为 AsyncRead 流。COPY 的一致性与性能语义与其他语言一致:单事务原子、最快写入通道、失败整批回滚、按行数切片分批。

sqlx:PgPoolOptions 与运行时查询 ​

rust
use sqlx::postgres::PgPoolOptions;
use std::time::Duration;

let pool = PgPoolOptions::new()
    .max_connections(20)
    .min_connections(4)
    .acquire_timeout(Duration::from_secs(3))
    .idle_timeout(Duration::from_secs(300))
    .max_lifetime(Duration::from_secs(1800))
    .connect(&url)
    .await?;

运行时查询 API 的四个入口:

rust
// execute:返回 QueryResult(rows_affected)
sqlx::query("INSERT INTO items (id, name) VALUES ($1, $2)")
    .bind(1i64).bind("alpha")
    .execute(&pool).await?;

// fetch_one / fetch_optional / fetch_all
let row: (i64, String) = sqlx::query_as("SELECT id, name FROM users WHERE id = $1")
    .bind(42).fetch_one(&pool).await?;
let users: Vec<(i64, String)> = sqlx::query_as("SELECT id, name FROM users")
    .fetch_all(&pool).await?;

query_as 的目标类型实现 FromRow(#[derive(sqlx::FromRow)] 即可),列名与字段名自动映射;bind 顺序对应 $1、$2,类型经由 Encode trait 校验。

sqlx:编译期校验宏 query!/query_as! ​

sqlx 的招牌能力——cargo build 时连接数据库解析 SQL,把列类型直接编进代码:

rust
// query!:列名与类型在编译期已知
let row = sqlx::query!(
    "SELECT id, name, amount FROM orders WHERE id = $1",
    order_id
)
.fetch_one(&pool)
.await?;

println!("{} {} {}", row.id, row.name, row.amount);   // 字段访问,类型精确
rust
// query_as!:映射到自定义结构体
#[derive(Debug)]
struct OrderSummary {
    id: i64,
    total: rust_decimal::Decimal,
}
let rows = sqlx::query_as!(
    OrderSummary,
    r#"SELECT id, sum(amount) AS "total!" FROM orders GROUP BY id"#
)
.fetch_all(&pool)
.await?;

宏的工程机制:构建时读取 DATABASE_URL(或环境变量 SQLX_OFFLINE=true 使用离线缓存)连库校验;离线缓存由 cargo sqlx prepare(sqlx-cli)生成(sqlx 0.7+ 存放在项目 .sqlx 目录)。CI 里 SQLX_OFFLINE=true 后,构建不再需要真实数据库。两个实用标注:

  • AS "col!":声明列非空(服务端校验不到 NOT NULL 表达式列时);
  • AS "col?":声明列可空(如聚合结果)。

代价是 SQL 与 schema 变更会触发重编译,团队要接受「改 SQL 就要重新 prepare」的流程。

deadpool-postgres:tokio-postgres 的池 ​

rust
use deadpool_postgres::{Manager, ManagerConfig, Pool, RecyclingMethod, Runtime};

let mut pg_config = tokio_postgres::Config::new();
pg_config.host = Some("db1".into());
pg_config.user = Some("app_rw".into());
pg_config.password = Some("secret".into());
pg_config.dbname = Some("appdb".into());

let mgr_config = ManagerConfig {
    recycling_method: RecyclingMethod::Verified,   // 取回连接时校验活性
};
let mgr = Manager::from_config(pg_config, tokio_postgres::NoTls, mgr_config);
let pool = Pool::builder(mgr).max_size(20).build()?;

// 使用:get 出来的是包装对象,Deref 到 tokio_postgres::Client
let client = pool.get().await?;
let rows = client.query("SELECT count(*) FROM users", &[]).await?;

RecyclingMethod::Fast(默认,直接复用)与 Verified(复用前跑一次轻量查询)的取舍是本池的核心配置:数据库会主动断开空闲连接的环境(防火墙、PgBouncer idle timeout)用 Verified 防止拿到死连接。pool.get() 超时、max_size 总账等容量语义与前面各语言一致。

JSONB:serde_json::Value 直通 ​

两套驱动的 JSONB 都能直通 serde_json::Value:

rust
// sqlx:开启 "json" 特性后,Value 可直接 bind/取值
let payload = serde_json::json!({"kind": "click", "n": 3});
sqlx::query("INSERT INTO events (payload) VALUES ($1)")
    .bind(&payload)
    .execute(&pool).await?;

let (val,): (serde_json::Value,) = sqlx::query_as(
    "SELECT payload FROM events WHERE id = $1",
).bind(1).fetch_one(&pool).await?;

// tokio-postgres:启用 "with-serde_json-1" 特性后同样直通
let row = client.query_one("SELECT payload FROM events WHERE id = $1", &[&1]).await?;
let val: serde_json::Value = row.get(0);

读取目标也可以是自定义结构体(serde::Deserialize),让数据库 JSONB 与 Rust 类型系统直连。写入前 serde_json::to_vec 手动序列化再传 Vec<u8> 的旧式写法在启用特性后不再必要。

常用类型映射速查 ​

PostgreSQL 类型Rust 类型(需 feature)
int8 / int4i64 / i32
text / varcharString / &str
boolbool
float8f64
numericrust_decimal::Decimal
timestamptzchrono::DateTime<Utc>(chrono feature)
uuiduuid::Uuid(uuid feature)
jsonbserde_json::Value(serde_json feature)
byteaVec<u8>
text[]Vec<String>

两套驱动的类型开关位置不同:sqlx 靠 Cargo feature(chrono、uuid、json、rust_decimal 等),tokio-postgres 靠 with-* 特性(如 with-serde_json-1、with-chrono-0_4)。特性没开时编译期就报「type not implemented」——这是 Rust 生态「编译期发现类型缺口」的又一例。

错误处理:SQLSTATE 的 Rust 表达 ​

tokio-postgres 的 Error::code() 返回 Option<&SqlState>,内置常量枚举覆盖全部 SQLSTATE:

rust
use tokio_postgres::error::SqlState;

match client.execute("INSERT INTO users (email) VALUES ($1)", &[&email]).await {
    Err(e) if e.code() == Some(&SqlState::UNIQUE_VIOLATION) => {
        return Err(AppError::EmailTaken);          // 23505
    }
    Err(e) if e.code() == Some(&SqlState::FOREIGN_KEY_VIOLATION) => {
        return Err(AppError::RefMissing);          // 23503
    }
    Err(e) if e.code() == Some(&SqlState::T_R_SERIALIZATION_FAILURE)
           || e.code() == Some(&SqlState::T_R_DEADLOCK_DETECTED) => {
        return Err(AppError::Retryable);           // 40001 / 40P01
    }
    Err(e) => return Err(e.into()),
    Ok(_) => {}
}

sqlx 侧用 DatabaseError::code():

rust
if let Err(e) = query.execute(&pool).await {
    if let Some(db_err) = e.as_database_error() {
        if db_err.code().as_deref() == Some("23505") {
            return Err(AppError::EmailTaken);
        }
    }
    return Err(e.into());
}

两套 API 都是「错误对象 → SQLSTATE → 业务错误」的映射套路,db_err.constraint()(约束名)、db_err.message() 提供更细的定位信息。

实战示例 ​

示例一:tokio-postgres 基础 CRUD 与事务 ​

rust
async fn transfer(
    client: &tokio_postgres::Client,
    from: i64, to: i64, amount: i64,
) -> Result<(), Box<dyn std::error::Error>> {
    let tx = client.transaction().await?;

    let n = tx.execute(
        "UPDATE accounts SET balance = balance - $1 WHERE id = $2 AND balance >= $1",
        &[&amount, &from],
    ).await?;
    if n == 0 {
        tx.rollback().await?;
        return Err("insufficient balance".into());
    }
    tx.execute(
        "UPDATE accounts SET balance = balance + $1 WHERE id = $2",
        &[&amount, &to],
    ).await?;

    tx.commit().await?;
    Ok(())
}

示例二:tokio-postgres copy_in 流式导入 ​

rust
use futures::SinkExt;
use tokio::io::AsyncReadExt;

async fn import_csv(
    client: &tokio_postgres::Client,
    path: &str,
) -> Result<u64, Box<dyn std::error::Error>> {
    let mut file = tokio::fs::File::open(path).await?;
    let mut buf = vec![0u8; 64 * 1024];

    let mut sink = client
        .copy_in("COPY stg_orders FROM STDIN WITH (FORMAT csv, HEADER true)")
        .await?;
    loop {
        let n = file.read(&mut buf).await?;
        if n == 0 { break; }
        sink.send(&buf[..n]).await?;
    }
    Ok(sink.finish().await?)
}

示例三:sqlx 编译期校验查询 ​

rust
#[derive(Debug, sqlx::FromRow)]
struct UserRow {
    id: i64,
    name: String,
    email: Option<String>,
}

async fn active_users(pool: &sqlx::PgPool) -> Result<Vec<UserRow>, sqlx::Error> {
    sqlx::query_as::<_, UserRow>(
        "SELECT id, name, email FROM users WHERE status = $1 ORDER BY id",
    )
    .bind("active")
    .fetch_all(pool)
    .await
}

示例四:sqlx 连接池 + 事务(实战完整形态) ​

rust
use sqlx::postgres::PgPoolOptions;
use std::time::Duration;

#[derive(Clone)]
pub struct Db(sqlx::PgPool);

impl Db {
    pub async fn new(url: &str) -> Result<Self, sqlx::Error> {
        let pool = PgPoolOptions::new()
            .max_connections(20)
            .min_connections(4)
            .acquire_timeout(Duration::from_secs(3))
            .idle_timeout(Duration::from_secs(300))
            .max_lifetime(Duration::from_secs(1800))
            .connect(url)
            .await?;
        Ok(Self(pool))
    }

    pub async fn archive_orders(&self, before: chrono::DateTime<chrono::Utc>)
        -> Result<u64, ArchiveError>
    {
        let mut tx = self.0.begin().await?;

        // 宏查询:列与类型编译期校验
        let moved = sqlx::query!(
            r#"WITH moved AS (
                   DELETE FROM orders
                   WHERE created_at < $1 AND archived = false
                   RETURNING id, user_id, amount, created_at
               )
               INSERT INTO orders_archive (id, user_id, amount, created_at)
               SELECT id, user_id, amount, created_at FROM moved
               RETURNING id"#,
            before
        )
        .fetch_all(&mut *tx)
        .await?;

        sqlx::query!("UPDATE orders SET archived = true WHERE created_at < $1", before)
            .execute(&mut *tx)
            .await?;

        tx.commit().await?;
        Ok(moved.len() as u64)
    }
}

要点:&mut *tx 把事务句柄以「独占借用」传入每条查询;commit 之前任何 ? 提前返回都会 drop 事务并自动回滚;fetch_all 收集 RETURNING 结果用于统计。

示例五:deadpool-postgres 池化服务层 ​

rust
use deadpool_postgres::{Manager, ManagerConfig, Pool, RecyclingMethod};

fn build_pool() -> Pool {
    let mut cfg = tokio_postgres::Config::new();
    cfg.host = Some("db1".into());
    cfg.dbname = Some("appdb".into());
    cfg.user = Some("app_rw".into());
    cfg.password = Some("secret".into());

    let mgr = Manager::from_config(
        cfg,
        tokio_postgres::NoTls,
        ManagerConfig { recycling_method: RecyclingMethod::Verified },
    );
    Pool::builder(mgr).max_size(20).build().unwrap()
}

注意事项 ​

Connection 必须被 await(spawn 推进)。 tokio_postgres::connect 返回的 connection 是网络循环本体,忘了 tokio::spawn 它会让所有查询永久挂起——这是 tokio-postgres 的第一坑。

Row::get 类型不匹配是运行时 panic。 按列标注取值类型的正确性由开发者负责;把这类校验前置到编译期的诉求正是 sqlx 宏存在的理由,混合团队建议业务查询一律走宏。

事务借用连接,事务内无法复用连接。 &mut client 的独占借用是安全保证;需要「事务中并行查询」的设计在 PostgreSQL 单连接语义下本就不成立,用 savepoint 或拆事务表达。

copy_in/copy_out 需要 futures 的 Sink/Stream 扩展。 sink.send 来自 futures::SinkExt;copy_out 的读取用 AsyncReadExt。COPY 单事务原子,失败整批回滚。

sqlx 宏依赖离线缓存的维护纪律。 cargo sqlx prepare 的产物(.sqlx)要进版本库并在 schema 变更后更新,否则 CI 的 SQLX_OFFLINE=true 构建会校验失败;这个流程要写进团队工作流。

deadpool 的回收策略决定死连接概率。 有 PgBouncer/防火墙空闲断连的环境用 RecyclingMethod::Verified;纯内网长连接可用默认 Fast。

numeric 与 timestamp 需要类型生态支持。 rust_decimal(numeric)、chrono/time(时间)、uuid、serde_json 都靠驱动 feature 开启,特性没开时的编译错误信息直白但容易遗漏。

连接超时三层防护照常适用。 acquire_timeout(池借出)、connect_timeout(建连)、服务端 statement_timeout(语句),Rust 侧没有隐式默认超时,不配就是无限等待。

两套驱动的 feature 清单要显式核对。 时间/数值/JSON 的类型支持都靠 feature 开启,升级依赖时 feature 名变化(如 with-chrono-0_4)会让编译错误排查变长;把 feature 清单固化在 Cargo.toml 注释或团队文档里。

tokio-postgres 原生没有自动重连。 连接断开就是断开(Connection 返回错误、池里的 deadpool 靠回收策略重建);服务要有「连接失效后重建池或退出交由进程管理器重启」的明确策略,别指望驱动自动复活。

优雅停机要排空在途事务。 SIGTERM 直接退出会让服务器回滚未决事务、掐断 COPY;先停止接新请求、等待在途事务完成(设兜底超时),再让连接自然关闭,比事后补偿便宜。

运行时拼接的 SQL 不享受宏校验。 query! 的编译期保证只覆盖写死的 SQL 文本;动态 SQL 改用 sqlx::query_as::<_, T>(&sql) 时类型校验责任回到开发者,这类函数要重点 review。

NOTIFY 载荷上限 8000 字节。 超长载荷直接报错;通知里只传 ID 与事件类型、订阅方回查详情是跨语言一致的标准做法。

编译期校验依赖的 schema 快照会过期。 .sqlx 缓存对的是「生成那一刻」的库结构,schema 迁移后忘更新缓存,CI 校验通过但运行时的库已经变了——prepare 与迁移放在同一条流水线里是根治法。

最佳实践 ​

  1. 业务服务首选 sqlx 一站式;ETL/COPY/NOTIFY 通道用 tokio-postgres。 两套池不要同时管同一个库的连接总量,先做连接总账。
  2. 连接参数显式声明 sslmode、connect_timeout、application_name。 与其他语言完全一致的四件套纪律。
  3. tokio-postgres 的 connection 永远 spawn 推进并记录错误日志。 把这一步固化成项目模板。
  4. 事务用「借用 + drop 自动回滚」语义写防御代码。 不 commit 的路径自然回滚,显式 rollback 只用于需要区分错误语义的场合。
  5. sqlx 宏覆盖常规查询,运行时 API 只留动态 SQL。 query! 的编译期校验是团队最重要的防回归资产。
  6. 离线模式进 CI:SQLX_OFFLINE=true + .sqlx 入库。 构建不依赖真实数据库,同时强制 schema 变更同步 prepare。
  7. 批量写入十万行以上一律 COPY。 copy_in 流式分块发送,配合摘索引、导后重建、ANALYZE 的通用清单。
  8. 错误处理按 SqlState/SQLSTATE 映射领域错误。 23505/23503/40001/40P01 的分支写进统一错误层,调用方只见业务错误。
  9. JSONB 直通 serde_json::Value 或自定义结构体。 开启对应 feature,杜绝手工序列化字符串。
  10. 池参数与 PostgreSQL 侧对账。 max_connections × 实例数 ≤ 服务端余量;多实例用 PgBouncer 收敛并收小应用池。
  11. 金额 numeric + rust_decimal,时间 timestamptz + chrono/time。 类型生态的对应关系写进项目依赖规范。
  12. 给每个服务独立 application_name。 pg_stat_activity 溯源在 Rust 服务同样是一排障利器。
  13. 把「重试哪些 SQLSTATE」写进统一的错误层。 40001/40P01 带退避重试、其余向上抛,重试逻辑集中在最外层而不是散在每个 handler。

下一节

继续学习:分页模式

参考链接 ​