怎么读这一卷
主线位置:三条主线的总览。本页把查询主线、写入主线和定义主线各画一遍,标出每一步落在哪个函数上、哪一页讲它;「一次 find 的全过程」用一个完整的例子把查询主线走一遍,并用抓包证明实际发出的 SQL。
flowchart TB
start[从这里开始] --> q{读这一卷为了什么}
q -->|会用| use1[01 全景]
use1 --> use2["02 实体与关系"]
use2 --> use3["04 查询 05 写入"]
use3 --> use4["06 的事务"]
q -->|读懂实现| all[00 到 11 按顺序]
锁定版本
| 项 | 本卷 | 本站 Cargo.lock |
|---|---|---|
sea-orm(及 sea-orm-macros、sea-orm-migration、sea-orm-cli、sea-orm-codegen) | 2.0.4,tag 2.0.4,commit db40a19(2026-09-27) | 1.1.20 |
sea-query | 1.0.2,tag 1.0.2,commit f2d5e04(2026-08-12) | 0.32.7 |
| 把 sea-query 的值绑到 sqlx 的 crate | sea-query-sqlx 0.9.1(sea-query 仓库的 sea-query-sqlx/ 目录) | sea-query-binder 0.7.0 |
sqlx | 0.9.0(上一卷) | 0.8.6 |
sea-schema(读数据库结构) | 0.18.1 | 0.16.2 |
rust-version / edition | sea-orm 1.94.0 / 2024;sea-query 1.88.0 / 2024 | sea-orm 1.1.20:1.81 / 2021 |
源码链接一律指向 tag:https://github.com/SeaQL/sea-orm/blob/2.0.4/...、https://github.com/SeaQL/sea-query/blob/1.0.2/...。sea-orm 的 Cargo.toml 写的是 sea-query = { version = "~1.0.0", ... }、sea-query-sqlx = { version = "0.9", ... }、sqlx = { version = "0.9.0", ... },所以 2.0.4 只能和 sea-query 1.0.x、sqlx 0.9 搭配。
本站用的是 1.1.20,实体是 1.x 的「展开」写法(Model + Relation 枚举 + Related impl)。1.1.20 到 2.0.4 是大版本:ConnectionTrait 的 execute / query_* 改收 sea-query 语句、DatabaseConnection 从 enum 变成 struct、新的 dense 实体格式(#[sea_orm::model])、ActiveModelEx 嵌套写入、Entity Loader、每个迁移一个事务、RBAC、同步版本 sea-orm-sync。本卷讲 2.0.4,讲到和 1.1.20 不同的地方会标出来;哪些改动落到本站、要改哪些代码,集中在 10 在本站中。
三层
一次查询从上到下经过三个库,本卷主要讲上面两层:
flowchart TB user["你的代码 Entity::find"] --> orm["sea-orm 实体 查询 执行"] orm -->|"SelectStatement 等"| sq["sea-query 语句树"] sq -->|"QueryBuilder 写 SQL"| stmt["Statement 加 Values"] stmt -->|"sea-query-sqlx 绑参数"| sqlx["sqlx query_with"] sqlx --> pg[(Postgres)]
- sea-orm:实体(宏生成的类型和 trait 实现)、查询和写入的构造器、执行器、连接与事务、迁移。它不自己写 SQL 字符串,而是把每个操作翻译成 sea-query 的语句。
- sea-query:和数据库无关的语句树(
SelectStatement、InsertStatement……),加上每种方言一个QueryBuilder,把树写成 SQL 和一组参数。它不依赖 sqlx,也不连库。 - sqlx:连接池、协议、预备语句缓存、类型编解码,上一卷从头讲过。本卷讲到池、事务的
BEGIN/ROLLBACK、协议消息、Encode/Decode时只链过去,不重讲。
只讲 Postgres(sqlx-postgres)。MySQL、SQLite、rusqlite 只在对比时提到。
三条主线
全卷沿三条线展开,每页开头都会标出本页在哪条主线上。
查询主线:一次 find_by_id(..).one(&db)
let cake: Option<cake::Model> = cake::Entity::find_by_id(1).one(&db).await?;
flowchart TB find["find_by_id 造 Select"] --> one["Selector::one 加 limit 1"] one --> qo["ConnectionTrait::query_one"] qo --> build["build 选 PostgresQueryBuilder"] build --> raw["DatabaseConnection 分派"] raw --> sq["sqlx_query 绑 SqlxValues"] sq --> acq["pool.acquire 借连接"] acq --> fetch["sqlx fetch_one"] fetch --> qr["PgRow 包成 QueryResult"] qr --> fqr["Model::from_query_result"]
| 步骤 | 源码 | 讲它的页 |
|---|---|---|
1. EntityTrait::find_by_id 是默认方法:Self::find()(Select::new 列出实体每一列、填 FROM),再按主键逐列 filter(col.eq(v)) | entity/base_entity.rs、query/select.rs | 实体的几个 trait、Select 与过滤 |
2. Select::one → into_model() 得到 Selector<SelectModel<E::Model>> → Selector::one:self.query.limit(1),db.query_one(&self.query) | executor/select.rs | 一次 find 的全过程 |
3. ConnectionTrait::query_one 是默认方法:get_database_backend().build(stmt),按后端选 sea-query 的 PostgresQueryBuilder,写出 SELECT "cake"."id", ... WHERE "cake"."id" = $1 LIMIT $2 和 Values,包成 Statement,再调 query_one_raw | database/connection.rs、database/statement.rs、sea-query backend/ | 语句是一棵树、QueryBuilder 与参数 |
4. DatabaseConnection::query_one_raw 按 inner 分派到 SqlxPostgresPoolConnection::query_one | database/db_connection.rs | DatabaseConnection 与 ConnectOptions |
5. sqlx_query = sqlx::query_with(AssertSqlSafe(sql), SqlxValues(values)),sea-query-sqlx 逐个 Value 绑进 PgArguments | driver/sqlx_postgres.rs、sea-query-sqlx/src/sqlx_postgres.rs | 绑到 sqlx |
6. SqlxPostgresPoolConnection::query_one:self.pool.acquire()(每条语句借一次),fetch_one(&mut *conn),RowNotFound 变成 Ok(None) | 同上 | 一次 find 的全过程、sqlx 卷获取连接 |
7. PgRow 包成 QueryResult,SelectModel 调 Model::from_query_result(&row, ""):DeriveEntityModel 生成,按列名逐列 try_get | executor/query.rs、executor/select.rs、宏 | DeriveEntityModel 与 dense 格式、列与值 |
第 6 步往下就是 sqlx 卷的查询主线和连接池主线:语句缓存、Parse / Bind / Execute、借出和归还时的 ping。
写入主线:一次 ActiveModel::insert / update / save
let am = cake::ActiveModel { name: Set("cheese cake".to_owned()), ..Default::default() };
let model: cake::Model = am.insert(&db).await?;
stateDiagram-v2 state "ActiveModel 每列一个 ActiveValue" as AM state "before_save" as BS state "Insert 只放 Set 的列" as INS state "UpdateOne 按主键 WHERE" as UPD state "RETURNING 拿回整行" as RET state "after_save" as AFT [*] --> AM AM --> BS BS --> INS: insert 或主键有 NotSet BS --> UPD: update 或主键齐全 INS --> RET UPD --> RET RET --> AFT AFT --> [*]
| 步骤 | 源码 | 讲它的页 |
|---|---|---|
1. ActiveModel 的每个字段是一个 ActiveValue<V>:Set(要写)、Unchanged(从库里读来、没改)、NotSet(不写) | entity/active_value.rs | ActiveValue 与 ActiveModel |
2. ActiveModelTrait::insert:before_save(self, db, true) → EntityTrait::insert 得到 Insert<A> → exec_with_returning(Postgres 支持 RETURNING,一条语句拿回整行)→ after_save | entity/active_model.rs、query/insert.rs、executor/insert.rs | insert |
3. ActiveModelTrait::update:before_save(self, db, false) → EntityTrait::update 得到 UpdateOne(主键列有 NotSet 时,PrimaryKeyNotSet 错误先存在里面,执行时才返回)→ exec_update_and_return_updated:UPDATE ... RETURNING,没有返回行就是 RecordNotUpdated | query/update.rs、executor/update.rs | update 与 delete |
4. ActiveModelTrait::save:is_update 看主键的每一列,有一列 NotSet 就 insert,否则 update;ActiveModelBehavior 的钩子在 ActiveModelBehavior | entity/active_model.rs | save 与 ActiveModelBehavior |
5. 2.0 的 ActiveModelEx:user::ActiveModel::builder().set_profile(..).add_post(..).save(&db),按外键依赖排顺序逐个写 | entity/active_model_ex.rs、宏 | 嵌套写入 ActiveModelEx |
写入同样经过查询主线的第 3 到 6 步:Insert / UpdateOne 里装的是 sea-query 的 InsertStatement / UpdateStatement,最后也是 query_with 加一次 acquire。update 时如果除主键外没有一列是 Set,exec_update_and_return_updated 根本不发 UPDATE,改为按主键查回原行,细节见 update 与 delete。
定义主线:一个实体从宏到 trait
#[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,
}
flowchart TB model["你写的 Model 结构体"] --> attr["sea_orm::model 属性宏"] model --> derive["DeriveEntityModel"] derive --> ent["Entity 单元结构体"] derive --> col["Column 枚举"] derive --> pk["PrimaryKey 枚举"] derive --> am["ActiveModel"] derive --> fqr["FromQueryResult 等实现"] attr --> ex["ModelEx ActiveModelEx"] attr --> rel["Relation EntityLoader"] rel --> rd["RelationDef"]
| 步骤 | 源码 | 讲它的页 |
|---|---|---|
1. DeriveEntityModel 转给 expand_derive_entity_model:从 Model 生成 Entity(单元结构体,实现 EntityName / EntityTrait)、Column(ColumnTrait)、PrimaryKey(PrimaryKeyTrait)、ActiveModel,以及 FromQueryResult、ModelTrait、ActiveModelTrait 等实现 | sea-orm-macros/src/derives/entity_model.rs 等 | 实体的几个 trait、DeriveEntityModel 与 dense 格式 |
2. 属性宏 sea_orm_model(2.0 新增,sea-orm 以 model 的名字导出,写作 #[sea_orm::model])再多生成 ModelEx、ActiveModelEx、EntityLoader、强类型列 COLUMN,并从 HasOne / HasMany / BelongsTo 字段生成 Relation | sea-orm-macros/src/derives/model_ex.rs 等 | DeriveEntityModel 与 dense 格式 |
3. 列:ColumnTrait 的 def() 给出 ColumnDef,eq / is_in / contains 把值变成 sea-query 的 Expr | entity/column.rs、entity/column_def.rs | 列与值 |
4. 关系:RelationDef 是运行时的描述(两端的表和列、join 类型),RelationTrait、Related 返回它;join、find_related、Loader 都只读它 | entity/relation.rs、entity/compound/ | 关系 |
一个两列的实体,加上 #[sea_orm::model],cargo expand 出来是 2 007 行、141 个 impl 块。定义主线是另外两条主线的前提:查询主线第 1 步的 Column::iter()、第 7 步的 from_query_result,写入主线的 ActiveModel 和 ActiveModelBehavior,都是这里生成的。
阅读路线
- 只想会用:读 01 全景,02 里的实体的几个 trait 和关系,04 查询,05 写入,06 里的事务,遇到报错再查 09 常见坑。
- 想读懂实现:按顺序读。02 是全卷的重点,三条主线都从宏生成的东西出发;03 讲 sea-query,是 sea-orm 和 sqlx 之间的那一层。
- 想自己写一个:读完 01、02 和 03,直接去 11 动手。
前置知识
- sqlx 卷的查询主线和连接池主线:
Executor、&Pool每次重新借连接、Transaction和Drop时的回滚。sea-orm 的每条语句最后都是一个sqlx::query_with(..).fetch_*(&mut *conn)。 - async/await 和 tokio。sea-orm 的
ConnectionTrait用#[async_trait::async_trait]声明,方法返回 boxed future。 - serde:
with-jsonfeature、ActiveModel::from_json、serdefeature,见 serde 卷。 - tracing:
tracing-spansfeature 给每条语句建 span,见 tracing 卷。
实验环境
关于行为的说法(发出了什么 SQL、几条语句、是 join 还是第二条查询、有没有 SAVEPOINT、报什么错、rustc 说了什么)都配一个小程序和它的真实输出。平台是 M2 Max、macOS、rustc 1.99.0、Postgres 16.14(本机容器)。实验 crate 的依赖锁定版本:
sea-orm = { version = "=2.0.4", features = ["sqlx-postgres", "runtime-tokio-rustls", "macros", "with-chrono", "with-json"] }
sea-query = "=1.0.2"
sqlx = { version = "=0.9.0", default-features = false, features = ["postgres", "runtime-tokio"] }
tokio = { version = "1", features = ["full"] }
四种看 SQL 的办法
同一个 cake::Entity::find_by_id(1).one(&db),有四种办法看它的 SQL,看到的东西不一样。
1. 不连库:build 和 to_string。 QueryTrait::build(DbBackend::Postgres) 返回 Statement,就是交给驱动的那一份;into_query().to_string(PostgresQueryBuilder) 把值内联进 SQL,只能用来读。注意 .one() 自己会加 limit(1),要看同一条 SQL 得手动加上:
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
2. sqlx 的语句日志。 sea-orm 的 ConnectOptions 默认 sqlx_logging(true)、sqlx_logging_level(log::LevelFilter::Info)(见 ConnectOptions::new),建池时交给 sqlx 的 log_statements;sqlx 自己的默认级别是 Debug,sea-orm 把它提到了 Info。程序里有一个 tracing_subscriber,RUST_LOG=sqlx=info 就能看到每条实际执行的语句,带 $N 占位符,不带参数值:
INFO sqlx::query: summary="SET search_path = \"so_intro\"" db.statement="" rows_affected=0 rows_returned=0 elapsed=531.5µs elapsed_secs=0.0005315
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.539417ms elapsed_secs=0.003539417
结果 Some(Model { id: 1, name: "cheese cake" })
第一条 SET search_path 是 set_schema_search_path 在每条新连接上执行的。日志格式见 sqlx 卷的怎么读这一卷。
3. debug-print feature。 驱动的每个执行方法开头都有一句 debug_print!("{}", stmt),开了这个 feature 时 debug_print! 展开成 tracing::debug!,target 是 sea_orm::driver::sqlx_postgres,内容是 Statement 的 Display。它不直接写标准输出,要 RUST_LOG=sea_orm=debug 加一个订阅者才看得到:
DEBUG sea_orm::driver::sqlx_postgres: create schema if not exists so_intro;
drop table if exists so_intro.cake;
create table so_intro.cake (id serial primary key, name text not null);
insert into so_intro.cake (name) values ('cheese cake');
DEBUG sea_orm::driver::sqlx_postgres: SELECT "cake"."id", "cake"."name" FROM "cake" WHERE "cake"."id" = 1 LIMIT 1
结果 Some(Model { id: 1, name: "cheese cake" })
不开 feature、只设 RUST_LOG=sea_orm=debug,这两行都没有。
4. MockDatabase。 开 mock feature,MockDatabase::new(DatabaseBackend::Postgres).append_query_results([..]).into_connection() 得到一个不连库的 DatabaseConnection,按顺序返回预置的结果,并记下每条语句。into_transaction_log() 拿到的是完整的 Statement,占位符和参数值都在:
mock 结果 Some(Model { id: 1, name: "cheese cake" })
[Transaction { stmts: [Statement { sql: "SELECT \"cake\".\"id\", \"cake\".\"name\" FROM \"cake\" WHERE \"cake\".\"id\" = $1 LIMIT $2", values: Some(Values([Int(Some(1)), BigUnsigned(Some(1))])), db_backend: Postgres }] }]
它不执行 SQL,所以证明不了 SQL 能跑、结果对,只能证明生成了什么,见 Mock 与 Proxy。
| 办法 | 要连库 | 占位符 | 参数值 | 看到的是 |
|---|---|---|---|---|
build / to_string | 否 | build 有,to_string 内联 | build 在 values 里 | 构造出来的语句,不经过 .one() 这类执行方法的加工 |
| sqlx 日志 | 是 | 有 | 没有 | sqlx 实际执行的每一条,包括 SET search_path 这类 sea-orm 内部发的 |
debug-print | 是 | 内联 | 内联 | 进驱动的每条 Statement |
MockDatabase | 否 | 有 | 有 | 进连接的每条 Statement,经过了执行方法的加工 |
本卷展示 SQL 时会说明是哪一种,优先用「实际发出的」:sqlx 日志、MockDatabase 的日志,或者一次 find 的全过程里那个能解出 Bind 参数字节的协议代理。
问: debug-print 打出来的 ... WHERE "cake"."id" = 1 LIMIT 1 可以直接复制到 psql 里跑,这不就是发给数据库的 SQL 吗?
答: 不是。Statement 的 Display 调 sea-query 的 inject_parameters,把 values 按 Postgres 的字面量写法塞回 $N 的位置,这是为了给人看。发给数据库的是 $1、$2 加上二进制格式的参数,SQL 文本里从来没有这些值,这也是 sea-orm 能对运行期拼出来的 SQL 用 AssertSqlSafe 的前提。两者在可读性上看不出差别,在行为上有差别:内联的字面量由服务端重新推断类型,参数的类型由客户端在 Bind 前定好(上面 LIMIT 的参数是 int8);内联的版本每换一个值就是另一条 SQL,参数版本始终是同一条,能命中语句缓存。排查「生成了什么」用它没问题,排查「数据库收到了什么」要看 sqlx 日志或抓包。
小结
- 全卷对着 sea-orm 2.0.4、sea-query 1.0.2、sea-query-sqlx 0.9.1、sqlx 0.9.0 写;本站用 1.1.20、sea-query 0.32.7、sea-query-binder 0.7.0、sqlx 0.8.6。
- 三层:sea-orm 把操作翻译成 sea-query 的语句树,sea-query 按方言写出 SQL 和参数,sqlx 借连接执行。
- 查询主线:
find_by_id→Selector::one(limit(1))→query_one里build→sqlx_query→ 每条语句acquire一次 →fetch_one→from_query_result。 - 写入主线:
ActiveValue三态 →before_save→Insert或UpdateOne+RETURNING→after_save;save看主键有没有NotSet。 - 定义主线:
DeriveEntityModel和#[sea_orm::model]生成Entity、Column、PrimaryKey、ActiveModel和一串 impl,关系落到RelationDef。 - 看 SQL 有四种办法,只有 sqlx 日志和抓包看到的是实际发出的;
debug-print是一条tracing::debug!,值是内联的。