Rust in Everything
基于 sea-orm 2.0.4

源码地图

主线位置:不在主线上。本页说明三条主线经过的代码分在哪几个 crate、哪些目录,crate 之间怎么依赖,feature 怎么把它们拼起来,以及同步版本 sea-orm-sync 是怎么来的。

sea-orm 仓库在 tag 2.0.4 的根 Cargo.toml 是一个工作区,members = [".", "sea-orm-macros", "sea-orm-codegen", "sea-orm-arrow"];sea-orm-migration、sea-orm-cli、sea-orm-sync、sea-orm-rocket 各自的 Cargo.toml 里又写着自己的 [workspace],是同一个仓库里的独立工作区。sea-query 在另一个仓库,sea-schema 在第三个仓库。和本卷有关的 crate 之间是这样依赖的(实线是普通依赖,虚线是可选依赖):

flowchart TB
  app["你的 crate"] --> orm["sea-orm"]
  app --> mig["sea-orm-migration"]
  orm --> mac["sea-orm-macros"]
  orm --> sq["sea-query"]
  orm -.->|"sqlx-postgres 等"| sqs["sea-query-sqlx"]
  orm -.->|"sqlx-postgres 等"| sqlx["sqlx"]
  orm -.->|"schema-sync"| schema["sea-schema"]
  sqs --> sq
  sqs --> sqlx
  mig --> orm
  mig --> schema
  mig -.->|"cli"| cli["sea-orm-cli"]
  cli --> gen["sea-orm-codegen"]
  cli --> schema
  cli --> sqlx
  gen --> sq

各个 crate

crate版本管什么源码行数
sea-orm2.0.4运行时:实体 trait、查询与写入的构造器、执行器、连接与事务、驱动适配、从实体建表src/ 45 625
sea-orm-macros2.0.4过程宏:DeriveEntityModel、#[sea_orm::model]、DeriveRelation、DeriveActiveEnum、DerivePartialModel、FromQueryResult 等src/ 12 665
sea-query1.0.2与数据库无关的语句树,加上 MySQL、Postgres、SQLite 三个 QueryBuildersrc/ 33 400
sea-query-sqlx0.9.1把 sea-query 的 Values 绑成 sqlx 的参数(SqlxValues)src/ 937
sea-query-derive1.0.0sea-query 的 #[derive(Iden)] 等src/ 2 339
sea-orm-migration2.0.4MigrationTrait、MigratorTrait、SchemaManager,可选内嵌 CLIsrc/ 2 388
sea-orm-cli2.0.4命令行 sea-orm-cli:从数据库生成实体、跑迁移src/ 1 770
sea-orm-codegen2.0.4把读到的表结构写成实体的 Rust 代码src/ 8 977
sea-schema0.18.1读数据库现有结构(discovery),codegen 和 schema-sync 用src/ 9 390
sea-orm-sync2.0.4同步版本,由脚本从 src/ 生成,见下文src/ 45 021,含测试和示例 69 823
sea-orm-arrow2.0.0-rc.5Arrow / Parquet 集成(本卷不讲)src/ 1 550
sea-orm-rocket0.6.0Rocket 集成(本卷不讲)649

行数是 find <dir> -name '*.rs' -print0 | xargs -0 cat | wc -l 在 tag 的解包上的结果,sea-query 的三个 crate 对的是 tag 1.0.2,sea-schema 对的是 crates.io 上的 0.18.1。

几条依赖关系值得记住:

  • sea-query 不依赖 sqlx。 它的 [dependencies] 全是可选的:sea-query-derive、sea-query-postgres-types,以及一串类型库(chrono、uuid、rust_decimal、serde_json……)。和驱动打交道的是单独的 sea-query-sqlx,它依赖 sea-query(开 thread-safe)和可选的 sqlx = "0.9"。sea-query 的 README 说「We provide integration for SQLx, postgres and rusqlite」,每一种都是这样一个独立的绑定 crate;sea-orm-sync 用的就是另一个,sea-query-rusqlite。
  • sea-orm-codegen 不依赖 sea-orm。 它只依赖 sea-query(用 sea-query 的类型描述列)和 syn / quote / prettyplease,产出的是 Rust 源码文本;sea-orm 只出现在它的 [dev-dependencies] 里。
  • sea-orm-cli 也不依赖 sea-orm。 它依赖 codegen、sea-schema 和 sqlx:连库读结构,交给 codegen 写文件。反过来,sea-orm-migration 的 cli feature 依赖 sea-orm-cli,迁移程序的命令行(up、down、fresh……)是从 cli 里拿的。
  • sea-orm 的 sqlx 是可选的。 sqlx、sqlx-core、sea-query-sqlx 都是 optional = true,由 sqlx-postgres 这类 feature 打开。只用默认 feature(不开任何驱动)的 crate 照样能编译,find_by_id(1).build(DbBackend::Postgres) 照样能打出 SQL,只是 Database::connect("postgres://...") 在运行时返回 Err(Conn(Internal("The connection string 'postgres://lab:lab@192.168.64.106/lab' has no supporting driver.")))。

本站的 1.1.20 里,绑定 crate 叫 sea-query-binder(本站 Cargo.lock 里是 0.7.0),2.0 换成了 sea-query-sqlx。

sea-orm 的 src/:每个目录回答一个问题

src/lib.rs 有 740 行,其中 674 行是 //! 文档(和 README 一样的内容),代码只是声明模块、pub use 一遍:database、driver、entity、error、executor、query、schema 都被整体重新导出,所以用户写 sea_orm::Select、sea_orm::DatabaseConnection,看不出它们在哪个目录。

目录行数回答的问题主要文件(行数)讲它的部分
entity/11 442一张表在 Rust 里是什么active_model.rs 2 586、base_entity.rs 1 288、column.rs 1 200、column_def.rs 699、active_value.rs 642、relation.rs 604、active_enum.rs 598、active_model_ex.rs 467、compound/(HasOne / HasMany / BelongsTo)、registry.rs02 实体、05 写入
query/6 793怎么构造一条语句loader.rs 1 864、helper.rs 922(QuerySelect、QueryFilter)、insert.rs 910、join.rs 905、select.rs 408、update.rs 365、delete.rs 31404 查询、05 写入
executor/10 951怎么执行,结果怎么变回 Rust 值cursor.rs 2 499、query.rs 1 902(QueryResult、TryGetable)、consolidate.rs 1 438、select.rs 1 223、paginator.rs 882、insert.rs 610、update.rs 42704 查询、05 写入
database/5 690连接、事务、mock 是什么db_connection.rs 926、mock.rs 886、transaction.rs 816、mod.rs 752(Database::connect、ConnectOptions)、restricted_connection.rs 602、connection.rs 250(ConnectionTrait)06 连接与事务
driver/3 482每种驱动怎么接上sqlx_postgres.rs 951、rusqlite.rs 749、sqlx_mysql.rs 551、sqlx_sqlite.rs 514、mock.rs 282、sqlx_common.rs 183(ConnectOptions 到 sqlx 池参数)、proxy.rs 160绑到 sqlx、06 连接与事务
schema/1 848怎么从实体建表、怎么同步结构builder.rs 686、entity.rs 507、topology.rs 43207 迁移与 schema
rbac/1 889按用户和角色限制能执行哪些语句engine/mod.rs 837、context.rs 322本卷不讲
dynamic/532编译期不知道表结构时怎么用模块文档写着「Unstable. The API in this module may change between minor versions.」本卷不讲
value/310几个额外的值类型text_uuid.rs(存成 TEXT 的 UUID)、时间戳包装列与值
tests_cfg/1 191上游测试和文档示例用的实体(cake、fruit、filling……)cake_expanded.rs 是手写的「宏展开」版本,和宏的实际输出不保证一致—

顶层还有 error.rs(349 行,DbErr,见错误)、docs.rs(173 行,四条设计原则的文档)、value.rs、metric.rs、util.rs(debug_print!)。

一条查询主线横穿其中四个目录:entity/base_entity.rs(find_by_id)→ query/select.rs(Select)→ executor/select.rs(Selector::one)→ database/connection.rs 和 db_connection.rs(query_one)→ driver/sqlx_postgres.rs(acquire + fetch_one)→ executor/query.rs(QueryResult)。这条线的逐步拆解见一次 find 的全过程。

sea-orm-macros/src/ 的 1.3 万行主要在 derives/ 下,每个派生一个文件:entity_model.rs、model_ex.rs(#[sea_orm::model])、active_model.rs、active_model_ex.rs、relation.rs、active_enum.rs、value_type.rs、partial_model.rs、from_query_result.rs、entity_loader.rs 等,见 DeriveEntityModel 与 dense 格式。sea-query 的 src/ 里,本卷用到的是 query/(8 237 行,各种语句)、expr/(2 624)、value/(2 152)、backend/(5 952,其中 query_builder.rs 2 114、postgres/ 1 566)和 prepare.rs(220),见 03 sea-query。

feature 体系

根 Cargo.toml 的 features 表大致分成下面几组。每个 feature 再用 crate/feature 或 crate?/feature 的写法转发给 sea-query、sea-query-sqlx、sea-orm-macros、sea-schema。

组feature说明
默认macros、with-json、with-chrono、with-rust_decimal、with-uuid、with-time、sqlite-use-returning-for-3_35、stream默认集里没有驱动,也没有运行时,两样都要自己开
驱动sqlx-postgres、sqlx-mysql、sqlx-sqlite、sqlx-allsqlx-postgres = sqlx-dep(打开 sqlx、sqlx-core)+ sea-query-sqlx/sqlx-postgres + sea-orm-macros/sqlx-postgres + postgres-array + sea-schema?/sqlx-postgres
运行时与 TLSruntime-tokio、runtime-tokio-rustls、runtime-tokio-native-tls、runtime-async-std 及其两个 TLS 组合runtime-tokio-rustls = sqlx?/tls-rustls + runtime-tokio
类型with-json、with-chrono、with-time、with-uuid、with-rust_decimal、with-bigdecimal、with-ipnetwork、with-mac_address、postgres-array、postgres-vector、with-arrow同时打开 sea-query 的同名 feature 和 sea-query-sqlx 的绑定
功能mock、proxy、debug-print、tracing-spans、schema-sync、entity-registry、rbac、seaography、serde、stream见下
行为开关postgres-use-serial-pk、sqlite-use-returning-for-3_35、mariadb-use-returning、sqlite-no-row-value-before-3_15改变生成的 SQL

功能那一组里,有的只是一个 cfg 开关,没有任何依赖:mock = []、debug-print = []、tracing-spans = []。它们在源码里各自打开一段 #[cfg(feature = "...")] 代码:mock 给 DatabaseConnectionType 加一个 MockDatabaseConnection 分支(这会改变这个 enum 的形状,见 Mock 与 Proxy),debug-print 让驱动里的 debug_print! 展开成 tracing::debug!,tracing-spans 让 with_db_span! 为每条语句建一个 span。另一些会拉进依赖:schema-sync = ["sea-schema"],entity-registry = ["inventory", "sea-orm-macros/entity-registry"](用 inventory 在链接期收集所有实体),rbac = ["sea-query/audit", "macros"](sea-query 的 audit 能列出一条语句访问了哪些表),stream = ["dep:ouroboros"](stream() 返回的流要同时持有连接和借用它的查询,用 ouroboros 做自引用结构)。

运行时和 TLS 在 sea-orm 里还是组合 feature(runtime-tokio-rustls),而 sqlx 0.9 已经把这类组合 feature 删掉了(见 sqlx 卷的源码地图)。sea-orm 这边是自己把组合拆开再转发:runtime-tokio-rustls 打开 sqlx?/tls-rustls 和 runtime-tokio,后者再打开 sqlx?/runtime-tokio。tls-rustls 在 sqlx 0.9 里是 tls-rustls-ring-webpki 的别名。本卷实验 crate 开的是 sqlx-postgres、runtime-tokio-rustls、macros、with-chrono、with-json,没有关默认 feature,所以默认集也在。

sea-orm-sync:用 sed 生成的同步版本

sea-orm-sync 是 sea-orm 的同步 API:没有 async、没有 .await,不需要异步运行时。它的 Cargo.toml 写着 name = "sea-orm-sync"、description = "🐚 The sync version of SeaORM",[lib] 的 name = "sea_orm",所以用户代码里照样写 use sea_orm::...。它的驱动只有 rusqlite:rusqlite = ["sea-query-rusqlite/sea-orm", "sea-schema-sync/rusqlite"],而 sqlx-postgres、sqlx-mysql、sqlx-sqlite、sqlx-dep 在这里都是空 feature,源码里的 driver/sqlx_postgres.rs 虽然也被复制了过来,却没有依赖能让它编译。README 的说法是「making it ideal for lightweight CLI programs with SQLite」。

这四万五千行不是手写的。build-tools/make-sync.sh 先删掉 sea-orm-sync/src 和 sea-orm-sync/tests,把 src、tests 和快速入门示例整个复制过去,然后对每个 .rs 文件跑六十多条 sed 替换,最后 cargo +nightly fmt。替换的核心几条:

replace_rs 's/Box::pin(async move {/({/' src
replace_rs 's/AsyncFnOnce/FnOnce/g' src
replace_rs 's/transaction_async/transaction/g' src tests
replace_rs 's/async //' src
replace_rs 's/\.await//' src
replace_rs '/#\[async_trait::async_trait/d' src
replace_rs 's/#\[tokio::test\]/#\[test\]/' src
replace_rs "s/type Stream<'a>: Stream/type Stream<'a>: Iterator/" src
replace_rs 's/ + Send//' src
replace_rs 's/Box::pin/Box::new/' src
replace_rs 's/impl Stream</impl Iterator</' src
replace_rs 's/use futures_util::lock::Mutex;/use std::sync::Mutex;/' src

也就是:把所有 async 和 .await 删掉,async_trait 属性删掉,Stream 换成 Iterator,Send / Sync 约束删掉,futures_util 的异步锁换成 std::sync::Mutex。这能行得通,靠的是 src/ 的写法配合脚本:异步代码里不出现「删掉 async 就不成立」的写法,脚本没法机械处理的地方,在源码里用 #[cfg(feature = "sync")] 分开写:src/ 里有 57 处,例如 database/tracing_spans.rs 的 with_db_span! 在同步版本里用 span.in_scope(..),异步版本里用 Instrument::instrument(..).await。

实验:重跑脚本,和发布的内容比较

在 tag 解包的一个副本里(src、tests、sea-orm-sync、sea-orm-macros、sea-orm-arrow、build-tools、.rustfmt.toml 和 examples/quickstart/src/main.rs,后两个 crate 是 cargo fmt 解析 sea-orm-sync 的工作区时要读的路径依赖),先把随 tag 发布的 sea-orm-sync 另存一份,再运行脚本:

cp -r sea-orm-sync shipped-sync
bash build-tools/make-sync.sh; echo "make-sync exit=$?"
for d in src tests examples; do
  diff -r shipped-sync/$d sea-orm-sync/$d > /dev/null && echo "$d: identical" || echo "$d: differs"
done
grep -rc "async fn" src | awk -F: '{s+=$2} END {print "async fn in src:", s}'
grep -rc "async fn" sea-orm-sync/src | awk -F: '{s+=$2} END {print "async fn in sync src:", s}'

M2 Max、macOS(脚本开头会检测 GNU / BSD sed,macOS 上用 sed -i '')上的输出:

make-sync exit=0
src: identical
tests: identical
examples: identical
async fn in src: 512
async fn in sync src: 0

重新生成的 src、tests、examples 和 tag 里发布的逐字节相同;src/ 里 512 行含 async fn,生成的版本里一行也没有。两边都是 145 个 .rs 文件,行数从 45 625 变成 45 021,少掉的是被删的 #[async_trait::async_trait]、use futures_util::... 这类行,再加上 rustfmt 把变短的签名合并成一行。

也就是说,2.0.4 的同步版本确实就是这个脚本的产物。仓库的 CI 只在 sea-orm-sync 目录里跑 cargo test --test '*' --features tests-features,rusqlite 和两个示例,并不重跑脚本比较差异:两份代码一致,靠的是改了 src/ 的人记得重新生成。

📝NOTE
冷知识:同步版本是和 rusqlite 驱动一起来的

make-sync.sh、sea-orm-sync/、src/driver/rusqlite.rs 是同一个提交加进仓库的:PR #2847「Rusqlite Support」(2025-12-13 合并),提交信息依次是「Init sea-orm-sync」「Stream and transaction」「All tests pass」「CI」「Support sea-schema-sync」。在这之前 sea-orm 只有异步 API,驱动都是 sqlx 的。PR 的说明只有一句「Almost, since sea-schema is still missing. But the core ORM works」,读结构用的 sea-schema-sync 是后来补上的。CHANGELOG 把它记在 2.0.0-rc.21 的 New Features 里:「New sea-orm-sync crate for synchronous SQLite via rusqlite」,同一节还写着「Stream and transaction support」「Integration with sea-schema-sync」。现在 src/driver/rusqlite.rs 放在异步的 src/ 里,但异步 crate 的 rusqlite = [] 不带任何依赖,文件里用的 sea_query_rusqlite(连同它重新导出的 rusqlite)只在同步版本的 Cargo.toml 里才有;脚本的倒数第二步还专门对这个文件做了一次反向替换(try_lock().ok() 改回 try_lock())。

小结

  • 主线上的 crate:sea-orm(运行时)、sea-orm-macros(派生和 #[sea_orm::model])、sea-query(语句树和方言)、sea-query-sqlx(绑参数)、sqlx(执行);迁移和代码生成是 sea-orm-migration、sea-orm-cli + sea-orm-codegen + sea-schema。
  • sea-query 不依赖 sqlx,绑定在单独的 crate 里;sea-orm 的 sqlx 依赖是可选的,由驱动 feature 打开。
  • src/ 按问题分目录:entity/ 是表,query/ 造语句,executor/ 执行并解码,database/ 是连接和事务,driver/ 接驱动,schema/ 建表和同步。
  • 默认 feature 里没有驱动和运行时;mock、debug-print、tracing-spans 是纯 cfg 开关,mock 会改变 DatabaseConnectionType 的形状。
  • sea-orm-sync 由 build-tools/make-sync.sh 用 sed 从 src/ 生成,在 2.0.4 上重跑脚本,结果和发布的逐字节相同;只有 rusqlite 驱动。

💬 关联讨论

发起讨论
加载中...