Rust in Everything
基于 sea-orm 2.0.4

sea-orm 源码解析

全卷只对着一组版本写:

crate版本tagcommit日期
sea-orm(及 sea-orm-macros、sea-orm-migration、sea-orm-cli、sea-orm-codegen)2.0.42.0.4db40a192026-09-27
sea-query1.0.21.0.2f2d5e042026-08-12
sea-query-sqlx0.9.1sea-query 仓库的 sea-query-sqlx/ 目录,源码和 1.0.2 tag 一致
sqlx0.9.0见 sqlx 卷

本站跑的是 sea-orm 1.1.20(sea-query 0.32.7、sqlx 0.8.6),实体是 1.x 的展开写法。1.1.20 到 2.0.4 是大版本,讲到不同的地方会标出来,哪些改动影响本站集中在 10 在本站中。只讲 Postgres(sqlx-postgres),MySQL、SQLite 只在对比时提到。

sea-orm 给自己的描述是「An async & dynamic ORM for Rust」:执行建立在 sqlx 上,查询是运行期构造的值。它自己不写 SQL 字符串,而是把每个操作翻译成 sea-query 的语句树,sea-query 按方言把树写成 SQL 和一组参数,再经 sea-query-sqlx 交给 sqlx 执行。所以这一卷讲两个库:sea-orm 是上层的实体和执行,sea-query 是中间那层语句树;最下层 sqlx 的连接池、协议、类型编码,链到上一卷,不重讲。

全卷沿三条线展开:

  • 查询主线(一次 find_by_id(..).one(&db)):EntityTrait::find_by_id 造一个 Select<E>,里面是 sea-query 的 SelectStatement,列出实体的每一列、按主键加 WHERE;.one(&db) 换成 Selector,先 limit(1),再 ConnectionTrait::query_one:到这里才按后端选 PostgresQueryBuilder,写出 SELECT "cake"."id", ... WHERE "cake"."id" = $1 LIMIT $2 和参数;驱动里 sqlx::query_with(..),每条语句单独 pool.acquire() 一次,fetch_one 拿回的 PgRow 包成 QueryResult,由宏生成的 FromQueryResult 按列名解码成 Model。
  • 写入主线(一次 ActiveModel::insert / update / save):ActiveModel 的每个字段是 ActiveValue 的三态之一:Set、Unchanged、NotSet。insert 只放 Set 的列,update 只 SET 被 Set 的列、按主键 WHERE,Postgres 上都用 RETURNING 一条语句拿回整行;save 看主键有没有 NotSet 决定走哪条;前后各有一个 ActiveModelBehavior 钩子。2.0 的 ActiveModelEx 把一组有外键关系的对象按依赖顺序逐个写。
  • 定义主线(一个实体从宏到 trait):用户只写一个 Model struct;DeriveEntityModel 从它生成 Entity(单元结构体,类型就是表)、Column 枚举、PrimaryKey 枚举、ActiveModel 和一串 trait 实现;2.0 的 #[sea_orm::model] 再生成 ModelEx、ActiveModelEx、EntityLoader、强类型列,并把 HasOne / HasMany / BelongsTo 字段变成关系。关系在运行时就是一个 RelationDef 值,join、find_related、Loader 都只读它。

三条线每一步对应的源码和页面,见怎么读这一卷。

flowchart TB
  intro[00 导读] --> overview[01 全景]
  overview --> entity[02 实体]
  overview --> sq["03 sea-query"]
  entity --> select[04 查询]
  sq --> select
  entity --> write[05 写入]
  sq --> write
  select --> conn["06 连接与事务"]
  write --> conn
  conn --> mig["07 迁移与 schema"]
  mig --> design[08 设计取舍]
  design --> pit[09 常见坑]
  pit --> site[10 在本站中]
  site --> hands[11 动手]

实验环境

关于行为的说法(发出了什么 SQL、几条语句、是 join 还是第二条查询、有没有 SAVEPOINT、报什么错、rustc 说了什么)都配一个小程序和它的真实输出,平台是 M2 Max、macOS、rustc 1.99.0、Postgres 16.14(本机容器)。看实际发出的 SQL 用 sqlx 的语句日志,要看参数字节和协议消息就用 sqlx 卷那个 tokio TCP 代理,做法见一次 find 的全过程;四种看 SQL 的办法各看到什么,见怎么读这一卷。with-json 和 from_json 链到 serde 卷,tracing-spans 链到 tracing 卷。

各部分

  1. 00 导读:锁定的版本和本站的差距,三层结构,三条主线每一步落在哪个函数,阅读路线、四种看 SQL 的办法;各 crate 的分工、src/ 的目录、feature 体系、sea-orm-sync 怎么从异步源码生成。
  2. 01 全景:sea-orm 和 sqlx、diesel 在设计上的分界,编译期能抓到什么;用抓包把一次 find_by_id(1).one(&db) 完整走一遍,LIMIT 是绑定参数,每条语句各借一次连接。
  3. 02 实体:本卷重点。EntityTrait 一族 trait,DeriveEntityModel 和 dense 格式的宏展开,列与值、TryGetable,关系与 RelationDef,DeriveActiveEnum 和 DeriveValueType。
  4. 03 sea-query:语句树的结构,QueryBuilder 怎么写出 SQL、哪些值变成参数,sea-query-sqlx 怎么把 Value 绑进 sqlx。
  5. 04 查询:Select 与过滤、Selector,join 与 find_related,Loader 和 2.0 的 Entity Loader,部分列与自定义结果,Paginator 与 Cursor。
  6. 05 写入:ActiveValue 三态,insert / insert_many / on_conflict,update 与 delete,save 与 ActiveModelBehavior,嵌套写入 ActiveModelEx。
  7. 06 连接与事务:DatabaseConnection 与 ConnectOptions,事务与保存点,DbErr,MockDatabase 与 Proxy。
  8. 07 迁移与 schema:sea-orm-migration,SchemaManager 与从实体建表,2.0 的 schema sync。
  9. 08 设计取舍:把各章的取舍汇总到一起,每条链回有证据的页面。
  10. 09 常见坑:用报错原文做标题,每个都先跑出最小复现。
  11. 10 在本站中:本站怎么用 sea-orm 1.1.20,生成的 SQL 有没有多余;1.1.20 到 2.0.4 的每条破坏性改动会不会影响本站。
  12. 11 动手:不依赖 sea-orm 和 sea-query,在 sqlx 0.9 上写一个迷你 ORM:Entity trait、Select 构造器、三态 ActiveValue 和只写改过的列的 update。

适合谁读

  • 用 sea-orm 写过代码,想知道 find().one(&db) 背后发了什么 SQL、update 为什么什么也没写、事务外的查询为什么看不到事务里的写入的读者:从导读开始,按阅读路线里「只想会用」那条读。
  • 想读懂一个 ORM 怎么实现的读者:按顺序读。02 是全卷的重点,三条主线都从宏生成的东西出发;03 是 ORM 和驱动之间那一层。
  • 需要的前置知识是 async/await、SQL 的基本概念,以及 sqlx 卷的查询主线和连接池主线:sea-orm 的每条语句最后都走那两条线。

💬 关联讨论

发起讨论
加载中...