升级到 2.0
主线位置:不在某一条主线上,而是把三条主线在本站的落点各过一遍:定义主线(11 个展开写法实体能不能原样编译)、查询主线和写入主线(站点的语句在 2.0.4 下发出的 SQL、返回的结果变没变),再加上迁移。sqlx 0.8.6 → 0.9.0 那一层见 sqlx 卷的升级到 0.9,本页不重复。
flowchart TB copy["拷一份工作区"] --> bump["两行版本号"] bump --> upd["cargo update 离线"] upd --> chk["cargo check"] chk --> errs["10 个错误"] errs --> fix["三行 use ExprTrait"] fix --> ok["9 个 crate 通过"] ok --> run["同一批实验在 2.0.4 上重跑"] run --> same["SQL 与结果相同"] run --> diff["迁移事务、错误文本"]
本页只给结论和最小改动清单,升不升、什么时候升由站长决定。所有实验都在 scratchpad 里的拷贝上做,本站仓库没有改动。平台:M2 Max、macOS、rustc 1.99.0、Postgres 16.14。
怎么试的
把 89a61b1 的整个工作区 git archive 到 scratchpad(exp-seaorm-site-up),先原样 cargo check --offline --locked -p app --no-default-features --features server,payments --tests,确认拷贝在 1.1.20 下能编译。然后只改两行:工作区 Cargo.toml 的 sea-orm = { version = "1.1.4", ... 改成 "2.0.4",crates/migration/Cargo.toml 的 sea-orm-migration = { version = "1.1", ... 改成 "2.0.4",feature 列表不动。离线更新锁文件,cargo update --offline -p sea-orm -p sea-orm-migration(只留 sea 和 sqlx 开头的行):
Locking 61 packages to highest compatible versions
Updating sea-orm v1.1.20 -> v2.0.4
Adding sea-orm-arrow v2.0.0-rc.5
Updating sea-orm-cli v1.1.20 -> v2.0.4
Updating sea-orm-macros v1.1.20 -> v2.0.4
Updating sea-orm-migration v1.1.20 -> v2.0.4
Updating sea-query v0.32.7 -> v1.0.2
Removing sea-query-binder v0.7.0
Updating sea-query-derive v0.4.3 -> v1.0.0
Adding sea-query-sqlx v0.9.1
Updating sea-schema v0.16.2 -> v0.18.1
Updating sqlx v0.8.6 -> v0.9.0
Updating sqlx-core v0.8.6 -> v0.9.0
Updating sqlx-macros v0.8.6 -> v0.9.0
Updating sqlx-macros-core v0.8.6 -> v0.9.0
Updating sqlx-mysql v0.8.6 -> v0.9.0
Updating sqlx-postgres v0.8.6 -> v0.9.0
Updating sqlx-sqlite v0.8.6 -> v0.9.0
note: pass `--verbose` to see 211 unchanged dependencies behind latest
锁文件里多出来的 sea-orm-arrow(以及 arrow-* 一族)是 sea-orm 的可选依赖,cargo tree -i arrow 在这个构建里什么也没打印,不会被编译;sqlx-mysql、sqlx-sqlite 在 1.1.20 的锁文件里本来就有。sea-orm-cli 也是原来就有的:sea-orm-migration 1.1.20 和 2.0.4 的默认 feature 都是 ["cli"]。实际打开的 feature(cargo tree -f '{p} [{f}]',节选):
sea-orm v2.0.4 [chrono,default,macros,postgres-array,runtime-tokio,runtime-tokio-rustls,rust_decimal,schema-sync,sea-query-sqlx,sea-schema,serde_json,sqlite-use-returning-for-3_35,sqlx,sqlx-core,sqlx-dep,sqlx-postgres,stream,time,uuid,with-chrono,with-json,with-rust_decimal,with-time,with-uuid]
sqlx v0.9.0 [_rt-tokio,chrono,json,postgres,runtime-tokio,rust_decimal,sqlx-postgres,time,tls-rustls,tls-rustls-ring,tls-rustls-ring-webpki,uuid]
本站 Cargo.toml 里写的 feature 名(sqlx-postgres、runtime-tokio-rustls、macros、with-chrono)在 2.0.4 里都还在,不用改。sea-orm 2.0.4 的 rust-version 是 1.94.0(1.1.20 是 1.81);本站 Dockerfile 的构建镜像是 rust:1-trixie,跟着最新稳定版走。
编译错误:10 个,一个原因
cargo check --offline -p app --no-default-features --features server,payments --tests,退出码 101。按错误码数(grep '^error' | sort | uniq -c):
2 error[E0599]: `sea_orm::sea_query::Expr` is not an iterator
2 error[E0308]: mismatched types
2 error[E0277]: the trait bound `sea_orm::Condition: From<bool>` is not satisfied
1 error[E0599]: no method named `add` found for enum `sea_orm::sea_query::Expr` in the current scope
1 error: could not compile `module-forum` (lib) due to 1 previous error
1 error: could not compile `module-course` (lib) due to 4 previous errors
1 error: could not compile `module-admin` (lib) due to 2 previous errors
七个真正的错误,落在三个函数里,形状各不相同,原因是同一个。论坛的 post_reply,计数加一:
error[E0599]: no method named `add` found for enum `sea_orm::sea_query::Expr` in the current scope
--> crates/modules/forum/src/server.rs:665:81
|
665 | .col_expr(topic::Column::ReplyCount, Expr::col(topic::Column::ReplyCount).add(1))
| ^^^ method not found in `sea_orm::sea_query::Expr`
|
::: ~/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/sea-query-1.0.2/src/expr/trait.rs:99:8
|
99 | fn add<R>(self, right: R) -> Expr
| --- the method is available for `sea_orm::sea_query::Expr` here
|
= help: items from traits can only be used if the trait is in scope
help: trait `ExprTrait` which provides `add` is implemented but not in scope; perhaps you want to import it
|
1 + use sea_orm::ExprTrait;
|
后台审核队列的两个 GROUP BY(第 628、638 行,两条一样,这里只贴一条)。Expr 自己没有 count 了,编译器在作用域里找到的唯一一个 count 是 Iterator::count:
error[E0599]: `sea_orm::sea_query::Expr` is not an iterator
--> crates/modules/admin/src/server.rs:628:60
|
628 | .column_as(Expr::col(moderation_queue::Column::Id).count(), "count")
| ^^^^^ `sea_orm::sea_query::Expr` is not an iterator
|
...
help: trait `ExprTrait` which provides `count` is implemented but not in scope; perhaps you want to import it
|
1 + use sea_orm::ExprTrait;
|
课程的标注列表最不直观。Expr 实现了 PartialEq,ExprTrait 不在作用域时,.eq(me_id) 被解析成 PartialEq::eq(&self, &Expr) -> bool,于是报的是参数类型不对,紧接着 Condition::add 又收到一个 bool。第 2857 行的 .ne("private") 同样各报一个 E0308、一个 E0277:
error[E0308]: mismatched types
--> crates/modules/course/src/server.rs:2856:53
|
2856 | .add(Expr::col(annotation::Column::UserId).eq(me_id))
| -- ^^^^^ expected `&Expr`, found `i32`
| |
| arguments to this method are incorrect
|
note: method defined here
--> ~/.rustup/toolchains/stable-aarch64-apple-darwin/lib/rustlib/src/rust/library/core/src/cmp.rs:258:8
|
258 | fn eq(&self, other: &Rhs) -> bool;
| ^^
error[E0277]: the trait bound `sea_orm::Condition: From<bool>` is not satisfied
--> crates/modules/course/src/server.rs:2856:12
|
2856 | .add(Expr::col(annotation::Column::UserId).eq(me_id))
| --- ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ the trait `From<bool>` is not implemented for `sea_orm::Condition`
| |
| required by a bound introduced by this call
...
help: consider removing this method call, as the receiver has type `sea_orm::sea_query::Expr` and `sea_orm::sea_query::Expr: From<bool>` trivially holds
|
2856 - .add(Expr::col(annotation::Column::UserId).eq(me_id))
2856 + .add(Expr::col(annotation::Column::UserId))
|
最后那条「考虑删掉这个方法调用」的建议是错的,照做会把过滤条件变成 "user_id" 本身;这一组错误里没有一条提到 ExprTrait。三处的共同点是方法挂在 Expr::col(..) 上:sea-query 1.0 里 Expr 是一个枚举,eq、ne、add、sub、count 这些方法只定义在 ExprTrait 上(Expr 的形状见语句是一棵树)。挂在实体列上的 topic::Column::Id.count()、Column::Status.eq(..) 走的是 ColumnTrait,不受影响,所以站点里大量的 filter(xxx::Column::Yyy.eq(..)) 一处都没报。sea-orm 2.0.4 在 src/query/mod.rs 里 pub use sea_query::ExprTrait,用 sea_orm::ExprTrait 就行。
最小改动是给这三个函数各加一个导入:
--- crates/modules/forum/src/server.rs
- use sea_orm::sea_query::Expr;
+ use sea_orm::{sea_query::Expr, ExprTrait};
--- crates/modules/admin/src/server.rs
- use sea_orm::{sea_query::Expr, QuerySelect};
+ use sea_orm::{sea_query::Expr, ExprTrait, QuerySelect};
--- crates/modules/course/src/server.rs
- use sea_orm::{sea_query::Expr, ColumnTrait, Condition, EntityTrait, QueryFilter, QueryOrder};
+ use sea_orm::{sea_query::Expr, ColumnTrait, ExprTrait, Condition, EntityTrait, QueryFilter, QueryOrder};
(位置是 forum 第 663 行、admin 第 622 行、course 第 2848 行。)改完再查,-p app 退出码 0;九个用到 sea-orm 的 crate 各自带上 server feature 跑 cargo check --all-targets(包括 module-moderation 连真库的集成测试和 app-core 的单测),也都是 0:
app-core exit=0 0 errors, 1 warnings
migration exit=0 0 errors, 1 warnings
module-admin exit=0 0 errors, 1 warnings
module-comments exit=0 0 errors, 1 warnings
module-course exit=0 0 errors, 1 warnings
module-forum exit=0 0 errors, 1 warnings
module-moderation exit=0 0 errors, 1 warnings
module-payment exit=0 0 errors, 1 warnings
module-search exit=0 0 errors, 1 warnings
每个 crate 的那一条警告都是 the following packages contain code that will be rejected by a future version of Rust: proc-macro-error2 v2.0.1,和 sea-orm 无关;cargo clippy -p app --all-targets 也只有这一条。没有一条 deprecation 警告。
Expr 的这些方法是被有意删掉的sea-query 0.32 里有两个类型:Expr(构造用的中间值)和 SimpleExpr(树里的节点),eq、add、count 等方法在 Expr 上有一份固有方法,在 0.32.0 加进来的 ExprTrait(sea-query#791,CHANGELOG 写「Added ExprTrait to unify Expr and SimpleExpr methods」)上又有一份,所以本站在 1.1.20 下不用导入任何 trait 就能写 Expr::col(..).eq(..)。sea-query#889 把 Expr 变成 SimpleExpr 的别名之后,sea-query#890「Remove inherent SimpleExpr methods that duplicate ExprTrait」删掉了重复的那份,PR 说明写着「This also removes these methods from Expr, because Expr has recenly become an alias of SimpleExpr (#889)」,迁移办法是「Where the methods are no longer found, just use sea_query::ExprTrait, as the compiler suggests」和「Where the methods conflict with PartialEq or PartialOrd methods, qualify them as TheTrait::the_method or use Rust's built-in operators」。它首次发布于 sea-query 1.0.0,sea-query 的 CHANGELOG 在这一条下面贴的第二段报错 expected &Expr, found &str 就是本站 course 那一处的样子。sea-orm 2.0.0 的发布说明也写了「for most compile errors, you can simply add use sea_orm::ExprTrait; in scope」。
实体不用动:11 个展开写法的实体在 2.0.4 下原样编译,宏生成的 SQL 也和 1.1.20 逐字相同(下一节)。dense 格式(#[sea_orm::model])是新增的写法,不是替代;改不改是另一件事,它会让宏输出翻倍、编译变慢(设计取舍),本站目前也用不到它带来的 Entity Loader 和 ActiveModelEx。
编译通过以后:哪些行为变了
把本站怎么用 sea-orm 的实验 crate 拷一份(exp-seaorm-site2),依赖换成 sea-orm 2.0.4、sea-orm-migration 2.0.4,代码一字不改(它用 use sea_orm::*,ExprTrait 随 glob 进来),在另一个 schema 里重跑同一批程序,把两边的输出逐行 diff:读路径的六组语句(queries_wire)、三处 on_conflict(upserts_wire)、事务和计数交错(txns)、find_also_related 吞错(also_related)都没有任何差别;并发首次登录(login_race)两边都是一个成功、一个 23505,只是哪个请求赢每次不一定。有差别的是迁移和错误文本。逐条对照 1.1.20 → 2.0.4 的改动:
| 改动 | 本站 | 依据 |
|---|---|---|
| 每个迁移一个事务(#2980,2.0.0-rc.36) | 会变 | 下一小节 |
DbErr 加了六个分支、#[non_exhaustive] | 不受影响 | 站点没有 match 过 DbErr,只构造 DbErr::Custom(pool.rs 和一个测试) |
DbErr 的 Display | 文本会变 | 模板没变;NULL 解码的文字变了(下文),sqlx 0.9 在数据库错误后面加 at line N |
count() 尊重 limit(#3097) | 不受影响 | 站点的 count() 都接在没有 limit 的 find() 后面;num_items 的 COUNT(*) 子查询两边逐字相同 |
delete_by_id 返回 ValidatedDeleteOne | 不受影响 | .exec(db) 照样得到 DeleteResult;删不存在的行两边都是 Ok(0) |
空 insert_many 不再发语句 | 不受影响 | 站点没有 insert_many |
eq(None) 生成 IS NULL | 不受影响 | 站点没有拿 Option 值做 eq / ne 的过滤 |
DatabaseConnection 从 enum 变 struct | 不受影响 | 站点只把它当类型传递和 clone(),没有按变体匹配 |
mock 不再去掉 Clone | 注释过时 | payment Cargo.toml 那条注释的理由不存在了(Mock 与 Proxy) |
find_also_related 吞右边的解码错误 | 没变 | 2.0.4 的 from_query_result_optional 还是 .ok(),also_related 两边都输出「已注销」 |
connect_timeout 落到 acquire_timeout | 没变 | DB_CONNECT_TIMEOUT_SECS 照样无效(DatabaseConnection 与 ConnectOptions) |
| sqlx 0.9 | 见 sqlx 卷 | 取消 begin 让连接带着事务回池的问题还在(升级到 0.9) |
count() 在 2.0.4 里仍然是 PaginatorTrait 的方法,站点 use sea_orm::PaginatorTrait 的写法照旧能用;行为差别见分页与游标。
迁移:一个大事务变成九个小事务
1.1.20 的 exec_with_connection 在 Postgres 上先 begin,把建记账表、读已应用列表和所有待执行的迁移都放进去,最后一次 commit。2.0.4 的 exec_up_with 对每个迁移问一次 should_use_transaction:迁移的 use_transaction() 返回 None(默认)时,Postgres 上就是 true,于是每个迁移连同它那一行 seaql_migrations 记录各自一个事务。机制在 sea-orm-migration。本站的 9 个迁移都没有覆盖 use_transaction。
同一个迁移 crate(两行版本号之外原样拷贝)对空 schema 跑一遍,2.0.4 下经代理(src/bin/migrate_wire.rs,节选):
wire> CREATE TABLE IF NOT EXISTS "seaql_migrations" ( "version" varchar NOT NULL PRIMARY KEY, "applied_at" bigint NOT NULL )
...
wire> SELECT "version", "applied_at" FROM "seaql_migrations" ORDER BY "version" ASC
wire> BEGIN
wire< ReadyForQuery(T)
...
wire> INSERT INTO "seaql_migrations" ("version", "applied_at") VALUES ($1, $2) RETURNING "version"
wire> COMMIT
wire< ReadyForQuery(I)
wire> BEGIN
wire< ReadyForQuery(T)
...
wire> INSERT INTO "seaql_migrations" ("version", "applied_at") VALUES ($1, $2) RETURNING "version"
wire> COMMIT
wire< ReadyForQuery(I)
...
Migrator::up = Ok(())
80 行输出里有 9 对 BEGIN / COMMIT,建记账表和读已应用列表移到了事务外面。全部成功时结果一样;差别在中途失败。设想生产库停在第 7 个迁移,一次部署带来第 8、9 个,而第 9 个失败了(实验里先手工加一个同名的 product_kind 列来制造失败,src/bin/partial.rs):
Migrator::up(&db, Some(7)).await?;
sql(&db, "alter table orders add column product_kind text").await?;
println!("Migrator::up = {:?}", Migrator::up(&db, None).await.map_err(|e| e.to_string()));
let applied: Vec<String> = Migrator::get_applied_migrations(&db).await?.iter().map(|m| m.name().to_string()).collect();
println!("applied: {} (last {:?})", applied.len(), applied.last());
// ... 再从 information_schema 查两列在不在
1.1.20 下:
Migrator::up = Err("Execution Error: error returned from database: column \"product_kind\" of relation \"orders\" already exists")
applied: 7 (last Some("m20260628_000007_memberships"))
users.token_version exists: false, orders.product_ref exists: false
第 9 个迁移的错误把同一个事务里的第 8 个也带走了:users.token_version 没有加上,库停在部署之前的样子。在 STRICT_MIGRATION=1 下这个实例启动失败,回滚二进制就能接着跑。
applied 是几个,token_version 在不在?同一个 partial.rs,依赖换成 sea-orm-migration 2.0.4,schema 换成 so_site_v2,其余一字不改。平台:M2 Max、macOS、rustc 1.99.0、Postgres 16.14。
这个差别对本站的影响要看迁移之间的依赖。第 9 个迁移把 orders.course_slug 改名成 product_ref,实体 order::Model 里已经是 product_ref;部分成功之后回滚到旧二进制,旧代码读的列都还在(第 8 个只是加列,第 9 个没有生效),不会出问题。以后写迁移时要记住:在 2.0 下,一次部署里的多个迁移不再同生同死,后面的失败不会撤销前面的;要原子,就把它们写进同一个迁移。反过来,2.0 允许 use_transaction() 返回 Some(false),可以写 CREATE INDEX CONCURRENTLY 了,1.1.20 下做不到。没有锁、没有校验和这两点 2.0.4 没有变(本站怎么用 sqlx)。
错误文本
DbErr 的 #[error(..)] 模板在两个版本之间一个字没改,只是 2.0.4 的 DbErr 多了六个分支。变的是装在里面的文字。站点的实体读到不该是 NULL 的 NULL(src/bin/misc.rs 的最后一行:把 users.nickname 改成可空、写一个 NULL,再 find_by_id),两边逐行 diff 只有这一行不同:
< NULL nickname: Err("Type Error: A null value was encountered while decoding \"nickname\"")
---
> NULL nickname: Err("Type Error: Missing value for column 'nickname'")
2.0 的实体 Model 先按 Option 读每一列,NULL 在 sea-orm 这一层被报成 Missing value for column(missing-value-for-column)。另一处是 sqlx 0.9 在数据库错误后面接的 at line N:上面迁移失败的那句话在 2.0.4 下是 ... column "product_kind" of relation "orders" already exists at line 7347(见动脑筋答案)。本站把 DbErr 的文本直接放进 ServerFnError(本站怎么用 sea-orm),这两处变化会出现在浏览器和日志里;站点代码没有按错误文本做判断的地方,逻辑不受影响。
最小改动清单
| 文件 | 改动 | 必须 |
|---|---|---|
Cargo.toml 第 49 行 | sea-orm 版本 "1.1.4" → "2.0.4" | 是 |
crates/migration/Cargo.toml 第 21 行 | sea-orm-migration 版本 "1.1" → "2.0.4" | 是 |
crates/modules/forum/src/server.rs 第 663 行 | 导入里加 ExprTrait | 是 |
crates/modules/admin/src/server.rs 第 622 行 | 导入里加 ExprTrait | 是 |
crates/modules/course/src/server.rs 第 2848 行 | 导入里加 ExprTrait | 是 |
crates/modules/payment/Cargo.toml 第 35 行 | 注释的理由在 2.0 下不成立,可以删掉或改写 | 否 |
| 以后的迁移 | 记住每个迁移一个事务;要一起成败的改动放进同一个迁移 | 否 |
Cargo.lock 由 cargo update -p sea-orm -p sea-orm-migration 生成,sqlx 随之到 0.9.0。上表之外,本站的 Rust 代码在这次升级里不需要任何改动。
动脑筋答案
Migrator::up = Err("Execution Error: error returned from database: column \"product_kind\" of relation \"orders\" already exists at line 7347")
applied: 8 (last Some("m20260721_000008_users_token_version"))
users.token_version exists: true, orders.product_ref exists: false
8 个,token_version 在。
- 第 8 个迁移在自己的事务里提交了,
seaql_migrations记下了它,users.token_version加上了。 - 第 9 个迁移的事务里,
RENAME COLUMN先成功、ADD COLUMN再失败,整个事务回滚,所以product_ref也不在,seaql_migrations里没有第 9 行。下一次启动只会重跑第 9 个。 - 错误文本末尾多了
at line 7347:这是 sqlx 0.9 的PgDatabaseError的Display接上的服务端 C 源码行号,1.1.20(sqlx 0.8.6)下同一个错误没有它。
小结
- 拷一份工作区,两行版本号改成 2.0.4,离线
cargo update,sqlx 随之到 0.9.0;站点写的 feature 名都还在。 - 编译错误 10 个,都在
Expr::col(..)后面的方法上:sea-query 1.0 把它们只留在ExprTrait里(sea-query#890)。.eq会被解析成PartialEq::eq,报的是类型不对。forum、admin、course 各加一个use sea_orm::ExprTrait就全部通过,测试和 clippy 也干净。 - 11 个展开写法的实体原样可用,不需要改 dense 格式。
- 站点的读路径、三处
on_conflict、事务和计数交错、find_also_related吞错,在两个版本上发出的 SQL 和结果逐字相同。 - 迁移从一个大事务变成每个一个事务:中途失败时前面的迁移留在库里,不再整体回滚。
- 错误文本有两处变化:NULL 解码从
A null value was encountered变成Missing value for column,数据库错误末尾多了at line N;DbErr新分支和non_exhaustive、count尊重limit、delete_by_id、空insert_many、DatabaseConnection改成 struct 都碰不到本站。