☰
SQLx 完全指南:用 Rust 实现异步数据库访问与编译期 SQL 校验
2026/10/1 9:21:46 网站建设 项目流程
  • 数据库
  • 后端

【免费下载链接】sqlx

🧰 The Rust SQL Toolkit. An async, pure Rust SQL crate featuring compile-time checked queries without a DSL. Supports PostgreSQL, MySQL, and SQLite.

项目地址:https://gitcode.com/gh_mirrors/sql/sqlx
点击查看免费下载

SQLx 是一个面向 Rust 的异步 SQL 工具包,主打"无需 DSL 的编译期查询校验",同时支持 PostgreSQL、MySQL/MariaDB 与 SQLite。本文以官方 README 为主线,结合仓库源码,系统讲解依赖与 feature 配置、连接池、查询 API、query!宏的编译期校验原理,以及配套的 sqlx-cli 离线构建工作流,帮助你从零搭建一个类型安全、可并发的数据库应用。

项目概览:SQLx 到底是什么

SQLx 的核心定位是async、pure Rust 的 SQL 库,它不提供 ORM 式的对象映射 API,而是直接以 SQL 字符串为输入,通过宏在编译期让数据库本身帮你验证查询。README 中概括了它的几大设计取向:

  • 真正异步:从底层起基于async/await构建,最大化并发能力。
  • 编译期检查查询(可选):即query!系列宏,见下文"SQLx 不是 ORM"一节。
  • 数据库无关:支持 [PostgreSQL]、[MySQL]、[MariaDB]、[SQLite];MSSQL 在 0.7 版本之前曾被支持,之后被移除,等待官方驱动重写。
  • 纯 Rust:Postgres 与 MySQL/MariaDB 驱动用纯 Rust 编写,且使用零unsafe代码(SQLite 例外,见"安全承诺"一节)。
  • 运行时无关:兼容 [async-std]、[tokio]、[actix] 等运行时,以及 [native-tls]、[rustls] 两种 TLS 后端。

此外还有一批面向工程化的能力,均有对应源码实现:

  • 内建连接池sqlx::Pool,实现位于 sqlx-core/src/pool/mod.rs。
  • 行流式读取:数据从数据库异步读出,按需解码。
  • 自动语句准备与缓存:使用高层查询 API(sqlx::query)时,语句按连接准备并缓存。
  • 简单(非 prepared)查询执行:同样支持批量执行并返回所有语句的结果。
  • TLS 支持:MySQL/MariaDB 与 PostgreSQL 均支持(需开启对应 TLS feature)。
  • 异步通知:PostgreSQL 的LISTEN/NOTIFY。
  • 嵌套事务:支持保存点(savepoint)。
  • Any驱动:可在运行时按 URL scheme 切换底层数据库驱动。

快速上手:安装与 Cargo Feature 组合

SQLx 兼容 [async-std]、[tokio]、[actix] 运行时,以及 [native-tls]、[rustls] TLS 后端。添加依赖时,必须选择一个"runtime + tls"的组合,README 给出了完整的示例清单:

# Cargo.toml [dependencies] # PICK ONE OF THE FOLLOWING: # tokio (no TLS) sqlx = { version = "0.9", features = [ "runtime-tokio" ] } # tokio + native-tls sqlx = { version = "0.9", features = [ "runtime-tokio", "tls-native-tls" ] } # tokio + rustls with ring and WebPKI CA certificates sqlx = { version = "0.9", features = [ "runtime-tokio", "tls-rustls-ring-webpki" ] } # tokio + rustls with ring and platform's native CA certificates sqlx = { version = "0.9", features = [ "runtime-tokio", "tls-rustls-ring-native-roots" ] } # tokio + rustls with aws-lc-rs sqlx = { version = "0.9", features = [ "runtime-tokio", "tls-rustls-aws-lc-rs" ] } # async-std (no TLS) sqlx = { version = "0.9", features = [ "runtime-async-std" ] } # async-std + native-tls sqlx = { version = "0.9", features = [ "runtime-async-std", "tls-native-tls" ] } # async-std + rustls with ring and WebPKI CA certificates sqlx = { version = "0.9", features = [ "runtime-async-std", "tls-rustls-ring-webpki" ] ] # async-std + rustls with ring and platform's native CA certificates sqlx = { version = "0.9", features = [ "runtime-async-std", "tls-rustls-ring-native-roots" ] } # async-std + rustls with aws-lc-rs sqlx = { version = "0.9", features = [ "runtime-async-std", "tls-rustls-aws-lc-rs" ] }

运行时与 TLS feature 的取舍

为保持向后兼容,runtime 与 TLS feature 既可以合并成一个 feature 使用,也可以分开声明。面向未来兼容,应使用分开的 runtime 与 TLS feature,因为合并型组合 feature 未来可能被移除。从仓库 Cargo.toml 的 feature 定义可以看到,runtime-tokio、runtime-async-std等独立 feature 与tls-native-tls、tls-rustls-*系列是平级拆分的。

运行时选择的具体规则见 src/lib.md:

  • 若同时启用多个运行时 feature,当前线程存在 Tokio 上下文(即tokio::runtime::Handle::try_current()返回Ok)时使用 Tokio,否则使用 async-std。
  • SQLx 不再对"零个或多个运行时 feature"产生编译错误(方便库作者二次封装),但几乎任何异步 API 在未启用至少一个运行时 feature 时调用都会 panic。唯一的例外是 SQLite 驱动(本身与运行时无关),不过SqlitePool的 timeout 与内部管理任务仍需要运行时支持。
  • TLS 方面,tls-native-tls使用操作系统原生 TLS(macOS 用 SecureTransport、Windows 用 SChannel、其他平台用 OpenSSL);tls-rustls是跨平台实现,仅支持 TLS 1.2 与 1.3。若使用 rustls 遇到HandshakeFailure,通常意味着数据库服务器不支持这些新版本,可尝试启用或切换tls-native-tls。同时启用多个 TLS feature 时tls-native-tls优先。若连接配置需要 TLS 升级但未启用 TLS feature,连接会直接返回错误。

Cargo Feature 全表

README 完整列出了各 feature 的语义,整理如下:

Feature说明
runtime-async-std使用async-std运行时,不启用 TLS 后端
runtime-tokio使用tokio运行时,不启用 TLS 后端(Actix-web 与 Tokio 完全兼容,无需单独的 runtime feature)
tls-native-tls使用native-tls后端(*nix 用 OpenSSL、Windows 用 SChannel、macOS 用 Secure Transport)
tls-rustls使用rustls后端(跨平台,仅支持 TLS 1.2 与 1.3)
tls-rustls-aws-lc-rs使用rustls+aws-lc-rs加密提供者
postgres支持 Postgres 数据库
mysql支持 MySQL/MariaDB(非 TLS 场景的 RSA 认证需要额外开启mysql-rsa)
mysql-rsa在 TLS 关闭时为caching_sha2_password/sha256_password启用 RSA 密码加密;仅在必须无 TLS 连接且服务器要求 RSA 认证时开启,优先建议使用 TLS
mssql支持 MSSQL 数据库
sqlite支持内嵌 SQLite,捆绑并静态链接 SQLite
sqlite-unbundled同上,但链接系统 SQLite 而非捆绑版本(可独立升级或使用 fork 版本;系统需安装 SQLite 或在构建时提供库路径;SQLite 过旧(低于 3.20.0)可能链接失败;因使用 bindgen 可能增加构建时间)
sqlite-preupdate-hook启用 SQLite 的 preupdate hook API(默认不开启;与sqlite-unbundled联用可能因系统 SQLite 版本不支持而链接失败)
any支持可在运行时代理到具体驱动的Any驱动
derive启用 derive 宏家族:FromRow、Type、Encode、Decode
macros启用query*!宏,实现编译期检查查询
migrate启用迁移管理与migrate!宏,支持编译期内嵌迁移
uuid支持 UUID 类型
chrono支持chronocrate 的日期时间类型
time支持timecrate 的日期时间类型(与chrono二选一;若两者都启用,query!宏默认偏好time)
bstr支持bstr::BString
bigdecimal使用bigdecimalcrate 支持NUMERIC
rust_decimal使用rust_decimalcrate 支持NUMERIC
ipnet支持 Postgres 的INET/CIDR(基于ipnet)
ipnetwork同上(基于ipnetwork)
json使用serde_json支持JSON/JSONB(Postgres)

注意 README 中特别说明:离线模式(offline mode)现已默认启用,详见 sqlx-cli/README.md 的离线构建章节。另外从仓库根 Cargo.toml 可以看到sqlx的默认 features 为["any", "macros", "migrate", "json"],即默认就带上Any驱动、查询宏、迁移能力与 JSON 支持。

编译期校验的设计哲学:SQLx 不是 ORM

README 用一个独立章节强调了这一设计立场:SQLx 支持编译期检查查询,但不是通过提供 Rust API 或 DSL 来构建查询,而是提供以普通 SQL 为输入的宏,并确保这些 SQL 对你的数据库是有效的。其工作方式是:在编译时连接你的开发数据库,让数据库自身去校验(并返回相关信息)SQL 查询。

这会带来两个值得注意的推论:

  • 由于 SQLx 从不自行解析 SQL 字符串,任何开发数据库能接受的语法都能使用,包括数据库扩展新增的语法。
  • 不同数据库允许查询方获取的信息量不同,因此查询宏能做的校验程度取决于数据库。

这一机制对应宏实现位于 sqlx-macros-core/src/query(含输入解析、数据库连接、元数据缓存与输出生成等模块)。如果你需要的是(异步)ORM,README 建议查阅官方 Ecosystem wiki 页(如ormx、SeaORM等)。

基础用法:连接、连接池与查询

仓库在 examples 目录提供了多数据库、多场景的完整示例(Postgres/MySQL/SQLite 的 todos、事务、监听、多数据库、多租户等),下面结合 README 的 Quickstart 与示例源码展开。

Quickstart

README 给出了一个最小可运行的程序:

use sqlx::postgres::PgPoolOptions; // use sqlx::mysql::MySqlPoolOptions; // etc. #[async_std::main] // Requires the `attributes` feature of `async-std` // or #[tokio::main] // or #[actix_web::main] async fn main() -> Result<(), sqlx::Error> { // Create a connection pool // for MySQL/MariaDB, use MySqlPoolOptions::new() // for SQLite, use SqlitePoolOptions::new() // etc. let pool = PgPoolOptions::new() .max_connections(5) .connect("postgres://postgres:password@localhost/test").await?; // Make a simple query to return the given parameter (use a question mark `?` instead of `$1` for MySQL/MariaDB) let row: (i64,) = sqlx::query_as("SELECT $1") .bind(150_i64) .fetch_one(&pool).await?; assert_eq!(row.0, 150); Ok(()) }

注意占位符的差异:Postgres 使用$1,MySQL/MariaDB 使用?。

建立连接

单个连接可通过任意数据库连接类型调用connect()建立(src/lib.rs 统一导出了各数据库的连接与池类型):

use sqlx::Connection; let conn = SqliteConnection::connect("sqlite::memory:").await?;

但实际项目中通常建议改用连接池(sqlx::Pool),以调控应用占用服务端连接的数量:

let pool = MySqlPool::connect("mysql://user:pass@host/database").await?;

连接池:为什么应该用 Pool

连接池模块的源码文档(sqlx-core/src/pool/mod.rs)详细解释了"为什么要用池":

  1. 开连接的代价高:对 SQLite 意味着大量文件系统请求与内存分配;对服务端数据库则涉及 DNS 解析、新 TCP 连接、缓冲区分配,以及复杂的握手(认证、连接参数协商、加密隧道升级)。服务端往往还要为每个连接派生线程/进程。
  2. 连接上限:MySQL/Postgres 等服务器通常对并发连接数设硬上限(如 Postgres 默认约 100,保留 3 个给超级用户)。用池可以让客户端在连接耗尽时进入公平等待队列,而不是直接报错或产生 500。
  3. 资源复用:prepared statement 与查询计划缓存通常按连接隔离,池促成了连接的复用,从而摊销准备语句的开销。

Pool本身是Send + Sync + Clone的引用计数句柄(内部为Arc<PoolInner<DB>>),建议在应用/服务启动时创建一次,然后共享给所有任务。&Pool可以直接传给任何需要Executor的地方,自动为你借出连接:

sqlx::query("DELETE FROM table").execute(&pool).await?;

池的默认参数定义在 sqlx-core/src/pool/options.rs 的PoolOptions::new()中,常用默认值如下:

配置项默认值语义
max_connections10池可维护的最大连接数(生产应用通常需要调高)
min_connections0池预建并尽力维持的最小连接数
test_before_acquiretrue借出连接前调用Connection::ping校验健康度
acquire_timeout30 秒acquire()等待连接的总时长上限,超时返回PoolTimedOut
acquire_slow_threshold2 秒超过该阈值视为"慢获取"并记日志
acquire_slow_levelWarn慢获取的日志级别
acquire_time_levelOff普通获取的日志级别(默认关闭)
idle_timeout10 分钟空闲连接在池中停留的上限,超时关闭(按用量计费的服务可省钱)
max_lifetime30 分钟连接最大生命周期,到期后回收(避免服务端内存/资源泄漏)
fairtrueacquire()是否公平(先到先得)

此外PoolOptions还支持三个回调:after_connect(连接建立后执行,如设置application_name、search_path等连接参数)、before_acquire(借出前对空闲连接执行检查,返回Ok(true)才借出)、after_release(归还时处理)。由于 Rust 无法直接表达带高阶生命周期的闭包返回类型,这些回调统一要求返回Box::pin的 future。

关于优雅关闭,Pool::close().await会唤醒所有等待者、拒绝后续acquire,并等待所有连接归还后向服务器发送关闭消息;由于没有 async drop,仅靠 drop 最后一个Pool句柄可能不会立刻通知服务端(服务端要等 TCP keepalive 超时),频繁创建销毁池还可能触发连接上限错误,因此文档明确建议在关闭阶段调用.close().await。

查询:prepared 与 unprepared

SQL 中的查询可分为prepared(参数化)与unprepared(简单)两类:

  • Prepared:查询计划会被缓存,使用二进制通信(带宽更低、解码更快),通过参数绑定避免 SQL 注入。
  • Unprepared:简单直接,仅用于 prepared 无法工作的场景,如PRAGMA、SET、BEGIN等数据库命令。

SQLx 对两者都支持。在 SQLx 中,&str被视为 unprepared 查询,Query/QueryAs结构体被视为 prepared 查询:

// low-level, Executor trait conn.execute("BEGIN").await?; // unprepared, simple query conn.execute(sqlx::query("DELETE FROM table")).await?; // prepared, cached query

应优先使用高层query接口,类型上有 finalizer(终结器),省去手动包 executor 的麻烦:

sqlx::query("DELETE FROM table").execute(&mut conn).await?; sqlx::query("DELETE FROM table").execute(&pool).await?;

execute返回受影响行数并丢弃所有结果;另有fetch、fetch_one、fetch_optional、fetch_all用于取回结果。sqlx::query返回的Query会产生Row<'conn>,列值可用row.get()按序号或列名访问;由于Row持有连接的不可变借用,同一时刻只能存在一个Row。

fetch返回类流类型,可逐行迭代:

// provides `try_next` use futures_util::TryStreamExt; // provides `try_get` use sqlx::Row; let mut rows = sqlx::query("SELECT * FROM users WHERE email = ?") .bind("user@example.com") .fetch(&mut conn); while let Some(row) = rows.try_next().await? { // map the row into a user-defined domain type let email: &str = row.try_get("email")?; }

把行映射为领域类型有两种惯用写法:

let mut stream = sqlx::query("SELECT * FROM users") .map(|row: PgRow| { // map the row into a user-defined domain type }) .fetch(&mut conn);
#[derive(sqlx::FromRow)] struct User { name: String, id: i64 } let mut stream = sqlx::query_as::<_, User>("SELECT * FROM users WHERE email = ? OR name = ?") .bind("user@example.com") .bind("example_username") .fetch(&mut conn);

若只需要单个结果,用fetch_one(必需结果)或fetch_optional(可选结果)。

事务

事务通过Connection::begin/Pool::begin开启,结束时应调用commit或rollback;若两者都未调用就离开作用域,drop时会自动回滚(见 sqlx-core/src/transaction.rs)。事务本身也实现了Executor,可以直接在其上执行查询:

let mut tx = conn.begin().await?; let result = sqlx::query("DELETE FROM \"testcases\" WHERE id = $1") .bind(id) .execute(&mut *tx) .await? .rows_affected(); tx.commit().await

嵌套事务通过保存点(savepoint)实现:TransactionManager的get_transaction_depth定义了深度语义——0 级无事务、1 级有活动事务、2 级及以上表示事务内建立了保存点。README 强调的"嵌套事务 + 保存点"能力即来自这一机制。

编译期校验的查询宏:query! 与 query_as!

这是 SQLx 最具特色的能力。使用sqlx::query!宏可以在编译期获得 SQL 的语法与语义双重校验,输出为匿名记录类型,每个 SQL 列对应一个 Rust 字段(必要时使用 raw identifier):

let countries = sqlx::query!( " SELECT country, COUNT(*) as count FROM users GROUP BY country WHERE organization = ? ", organization ) .fetch_all(&pool) // -> Vec<{ country: String, count: i64 }> .await?; // countries[0].country // countries[0].count

与query()的差异:

  • 绑定参数必须一次性全部给出,且编译期会校验参数的数量与类型是否正确。
  • 输出类型是匿名记录,上例中类型形如{ country: String, count: i64 }。
  • 构建时必须设置DATABASE_URL环境变量指向一个可供准备语句的数据库:该库不必有数据,但必须与运行时连接的库同类型(MySQL、Postgres 等)且同 schema。

为了方便,可以用 [.env文件](基于dotenvycrate,格式与dotenv相同)持久化DATABASE_URL:

DATABASE_URL=mysql://localhost/my_database

query!()最大的缺点是输出类型无法命名(Rust 尚未官方支持匿名记录),因此提供了query_as!()宏,除可命名输出类型外其余行为基本一致:

// no traits are needed struct Country { country: String, count: i64 } let countries = sqlx::query_as!(Country, " SELECT country, COUNT(*) as count FROM users GROUP BY country WHERE organization = ? ", organization ) .fetch_all(&pool) // -> Vec<Country> .await?; // countries[0].country // countries[0].count

各数据库下的占位符与类型差异

从仓库示例可看到不同数据库的细节差异:

  • Postgres使用$1占位符,且可用RETURNING id直接拿回插入的 id(见 examples/postgres/todos/src/main.rs)。
  • MySQL/MariaDB使用?占位符,插入后通过.last_insert_id()获取自增 id;且 MySQL 的布尔值实际存储为TINYINT(1)/i8,0 为 false、非 0 为 true,因此读取done字段时要与!= 0比较(见 examples/mysql/todos/src/main.rs)。

以 Postgres todos 为例,一个完整的 CRUD 流程长这样:

async fn add_todo(pool: &PgPool, description: String) -> anyhow::Result<i64> { let rec = sqlx::query!( r#" INSERT INTO todos ( description ) VALUES ( $1 ) RETURNING id "#, description ) .fetch_one(pool) .await?; Ok(rec.id) } async fn complete_todo(pool: &PgPool, id: i64) -> anyhow::Result<bool> { let rows_affected = sqlx::query!( r#" UPDATE todos SET done = TRUE WHERE id = $1 "#, id ) .execute(pool) .await? .rows_affected(); Ok(rows_affected > 0) }

加速增量编译

编译期校验在编译期做了不少工作。README 建议在Cargo.toml中加入以下配置,让cargo check、cargo build的增量构建显著变快:

[profile.dev.package.sqlx-macros] opt-level = 3

离线模式(offline mode)

如果项目代码(数据库访问部分)没有改动,却仍需要开发数据库才能编译,可以启用"离线模式",用 sqlx 命令行工具把 SQL 查询分析结果缓存下来。详细步骤见 sqlx-cli/README.md:

  1. 保存查询元数据:cargo sqlx prepare(必须通过cargo sqlx调用)。它会将查询元数据写入当前目录的.sqlx;若工作区有多个 crate 使用查询宏,加--workspace会在工作区根生成统一的.sqlx目录。把该目录提交进版本控制后,构建项目不再需要活动的数据库连接。
  2. 构建:之后正常cargo build即可。

CI 场景可用cargo sqlx prepare --check(或--check --workspace):若.sqlx中的数据与当前数据库 schema 或项目中的查询不一致,会以非零退出码结束。

另外,DATABASE_URL环境变量的优先级高于.sqlx目录——只要它存在,SQLx 默认仍会尝试连数据库构建。若想强制离线,可设置SQLX_OFFLINE=true(也可写入.env文件作为默认),cargo sqlx prepare本身不受影响、仍会正常连库。对于 feature flag 或测试中的查询,可通过给 cargo 传参来让 prepare 覆盖到,例如cargo sqlx prepare -- --all-targets --all-features。

进阶能力:Listen/Notify、Any 驱动与迁移

  • PostgreSQL 异步通知:通过LISTEN/NOTIFY实现,仓库示例见 examples/postgres/listen。
  • Any 驱动:AnyPool依据 URL scheme 在运行时选择底层驱动,相关实现位于 src/any 与各驱动的any.rs。
  • 迁移(migration):sqlx-cli 提供sqlx migrate add(生成migrations/<timestamp>-<name>.sql)、sqlx migrate run(对比数据库迁移历史并执行待应用脚本)、--source指定迁移目录、-r生成可逆迁移(.up.sql/.down.sql,之后所有迁移都会是可逆的)以及sqlx migrate revert。数据库的创建/删除可用sqlx database create/sqlx database drop,所有命令都通过--database-url或DATABASE_URL(环境变量或.env文件)提供连接信息。仓库各示例目录下都有真实的迁移文件可参考,如 examples/postgres/todos/migrations/20200718111257_todos.sql。

安全承诺与许可

README 与 src/lib.rs 都确认:SQLx 使用#![forbid(unsafe_code)]保证核心代码 100% Safe Rust。启用sqlitefeature 后会降级为#![deny(unsafe_code)]并仅对sqlx::sqlite模块放行unsafe——因为 SQLite 是嵌入式数据库,纯 Rust 只能靠移植整个 SQLite 实现,SQLx 的实际做法是通过libsqlite3-sys直接调用 SQLite3 C API。SQLx 官方欢迎社区对 unsafe 用法进行审计(相关调用点集中在 sqlx-sqlite 的语句句柄与连接实现中)。

SQLx 采用Apache-2.0 或 MIT双许可,以你选择的方式授权,许可文本见 LICENSE-APACHE 与 LICENSE-MIT。除非明确另行声明,按 Apache-2.0 定义提交的贡献均按上述双许可授权。

总结

SQLx 的竞争力来自三件事的叠加:纯 Rust 异步驱动(连接、流式读取、池、事务全套齐备)、以数据库为裁判的编译期查询校验(query!/query_as!+ 离线缓存),以及跨数据库、跨运行时、跨 TLS 后端的可组合 feature 体系。上手路径很清晰:先按 README 的组合表配好依赖,用PgPoolOptions之类建池,再用query!享受编译期保障;遇到无库可连的 CI 场景,就把cargo sqlx prepare与SQLX_OFFLINE纳入构建流程。

  • 数据库
  • 后端

【免费下载链接】sqlx

🧰 The Rust SQL Toolkit. An async, pure Rust SQL crate featuring compile-time checked queries without a DSL. Supports PostgreSQL, MySQL, and SQLite.

项目地址:https://gitcode.com/gh_mirrors/sql/sqlx
点击查看免费下载

相关推荐

上一篇:PptxGenJS 安装配置教程:5 分钟上手这款 JavaScript PPT生成库
下一篇:connectedhomeip 的 CHIPTool Android 演示应用:jniLibs 原生库目录结构与 ABI 组织指南

创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询