save 与 ActiveModelBehavior
主线位置:写入主线的入口和两端。
save是insert和update前面的一个分岔:它只看主键,选一条路走下去。不管走哪条,ActiveModelTrait的方法都在语句前后各调一次ActiveModelBehavior的钩子。ActiveValue三态见 ActiveValue 与 ActiveModel,两条路本身(INSERT ... RETURNING、UPDATE ... WHERE 主键)见 insert 和 update 与 delete,这一页只讲分岔和钩子。
flowchart TB
save["am.save(db)"] --> isup{"主键有 NotSet?"}
isup -->|有| ins["insert(db)"]
isup -->|没有| upd["update(db)"]
ins --> bs1["before_save(true)"]
bs1 --> sql1["INSERT RETURNING"]
sql1 --> as1["after_save(true)"]
upd --> bs2["before_save(false)"]
bs2 --> sql2["UPDATE RETURNING"]
sql2 --> as2["after_save(false)"]
as1 --> back["into_active_model"]
as2 --> back
实验 crate 依赖 sea-orm =2.0.4(sqlx-postgres、runtime-tokio-rustls、macros、with-json、postgres-array、serde),平台 M2 Max、macOS、rustc 1.99.0、Postgres 16.14。表有三张:note(id serial 主键、title、revision)、audit(钩子往里记一笔)、pair(复合主键 (a, b),都不自增)。下面的 wire> 行是一个夹在 sqlx 和 Postgres 之间的 TCP 代理打印的:客户端每执行一条语句(Query 消息,或者 Bind 一条解析过的语句)打一行 SQL,wire< 行是服务端的 ErrorResponse 和 ReadyForQuery 里事务状态的变化(I 空闲、T 事务中、E 事务已失败)。代理先打印再转发,所以 wire> 和钩子里 println! 的先后就是真实的先后。用代理而不是 sqlx 日志,是因为 sqlx 日志里没有 BEGIN(原因见 sqlx 卷的忘了 commit),而本页要看事务边界。
save:只看主键里有没有 NotSet
ActiveModelTrait::save 只有一个 if:
/// Insert the model if primary key is `NotSet`, update otherwise.
/// Only works if the entity has auto increment primary key.
async fn save<'a, C>(self, db: &'a C) -> Result<Self, DbErr>
where
<Self::Entity as EntityTrait>::Model: IntoActiveModel<Self>,
Self: ActiveModelBehavior,
C: ConnectionTrait,
{
let res = if !self.is_update() {
self.insert(db).await
} else {
self.update(db).await
}?;
Ok(res.into_active_model())
}
/// Returns true if the primary key is fully-specified
#[doc(hidden)]
fn is_update(&self) -> bool {
let mut is_update = true;
for key in <Self::Entity as EntityTrait>::PrimaryKey::iter() {
let col = key.into_column();
if self.is_not_set(col) {
is_update = false;
break;
}
}
is_update
}
is_update 逐个看主键列,只要有一列是 NotSet 就判成 insert。Set 和 Unchanged 在这里没有区别,不看其他列,也不查数据库里有没有这一行。save 的返回值不是 Model,是 insert / update 拿回的 Model 再 into_active_model(),所有字段都是 Unchanged。所以可以接着改、接着 save,第二次主键已经是 Unchanged,就走 update:
let am = note::ActiveModel { id: NotSet, title: Set("first".into()), revision: NotSet };
let saved = am.save(&db).await?;
println!("returned: {saved:?}");
let mut am = saved;
am.title = Set("first, edited".into());
let saved = am.save(&db).await?;
println!("returned: {saved:?}");
--- 1. save with id NotSet
[hook] before_save insert=true id=NotSet title=Set("first")
wire> INSERT INTO "note" ("title", "revision") VALUES ($1, $2) RETURNING "id", "title", "revision"
[hook] after_save insert=true model=Model { id: 1, title: "first", revision: 1 }
wire> INSERT INTO audit (note_id, action) VALUES ($1, $2)
returned: ActiveModel { id: Unchanged(1), title: Unchanged("first"), revision: Unchanged(1) }
--- 2. save the returned ActiveModel after changing title
[hook] before_save insert=false id=Unchanged(1) title=Set("first, edited")
wire> UPDATE "note" SET "title" = $1, "revision" = $2 WHERE "note"."id" = $3 RETURNING "id", "title", "revision"
[hook] after_save insert=false model=Model { id: 1, title: "first, edited", revision: 2 }
wire> INSERT INTO audit (note_id, action) VALUES ($1, $2)
returned: ActiveModel { id: Unchanged(1), title: Unchanged("first, edited"), revision: Unchanged(2) }
revision 两次都不是调用方给的:第一次是 NotSet,第二次是 Unchanged(1),是 before_save 改成了 Set(1) 和 Set(2)(钩子的代码在后面)。audit 那条 INSERT 是 after_save 发的。
源码注释里那句「Only works if the entity has auto increment primary key」说的是复合主键或手填主键的表。拿 pair 试三种情况:
let r = pair::ActiveModel { a: Set(1), b: NotSet, v: Set("x".into()) }.save(&db).await;
println!("result: {r:?}");
let r = pair::ActiveModel { a: Set(1), b: Set(2), v: Set("x".into()) }.save(&db).await;
println!("result: {r:?}");
let m = pair::ActiveModel { a: Set(1), b: Set(2), v: Set("x".into()) }.insert(&db).await?;
println!("inserted: {m:?}");
let r = pair::ActiveModel { a: Set(1), b: Set(2), v: Set("y".into()) }.save(&db).await;
println!("result: {r:?}");
这一段看的是 sqlx 日志(sqlx=info,每条语句一个事件;建表和 SET search_path 那几行删掉了):
--- 1. a Set, b NotSet: save
summary="INSERT INTO \"pair\" (\"a\", …" db.statement="\n\nINSERT INTO \"pair\" (\"a\", \"v\") VALUES ($1, $2) RETURNING \"a\", \"b\", \"v\"\n" rows_affected=0 rows_returned=0 elapsed=2.112125ms elapsed_secs=0.002112125
result: Err(Query(SqlxError(Database(PgDatabaseError { severity: Error, code: "23502", message: "null value in column \"b\" of relation \"pair\" violates not-null constraint", detail: Some("Failing row contains (1, null, x)."), hint: None, position: None, where: None, schema: Some("so_write2"), table: Some("pair"), column: Some("b"), data_type: None, constraint: None, file: Some("execMain.c"), line: Some(2057), routine: Some("ExecConstraints") }))))
--- 2. a and b Set, row does not exist: save
summary="UPDATE \"pair\" SET \"v\" …" db.statement="\n\nUPDATE \"pair\" SET \"v\" = $1 WHERE \"pair\".\"a\" = $2 AND \"pair\".\"b\" = $3 RETURNING \"a\", \"b\", \"v\"\n" rows_affected=0 rows_returned=0 elapsed=866.958µs elapsed_secs=0.000866958
result: Err(RecordNotUpdated)
--- 3. same values: insert, then save
summary="INSERT INTO \"pair\" (\"a\", …" db.statement="\n\nINSERT INTO \"pair\" (\"a\", \"b\", \"v\") VALUES ($1, $2, $3) RETURNING \"a\", \"b\", \"v\"\n" rows_affected=1 rows_returned=1 elapsed=893.333µs elapsed_secs=0.000893333
inserted: Model { a: 1, b: 2, v: "x" }
summary="UPDATE \"pair\" SET \"v\" …" db.statement="\n\nUPDATE \"pair\" SET \"v\" = $1 WHERE \"pair\".\"a\" = $2 AND \"pair\".\"b\" = $3 RETURNING \"a\", \"b\", \"v\"\n" rows_affected=1 rows_returned=1 elapsed=688.166µs elapsed_secs=0.000688166
result: Ok(ActiveModel { a: Unchanged(1), b: Unchanged(2), v: Unchanged("y") })
| 主键状态 | save 走 | 新行的结果 | 已有行的结果 |
|---|---|---|---|
任一列 NotSet | insert | 自增主键:数据库生成;手填主键:23502 非空约束 | 自增主键:插成第二行 |
全部 Set / Unchanged | update | RecordNotUpdated | 按主键更新 |
复合主键只设一列,save 照样判成 insert,INSERT 里干脆没有 b。两列都设上,save 一律判成 update,表里没有这一行就得到 RecordNotUpdated:save 没法插入一条主键由调用方给出的新行。这种表要新建就直接调 insert;要「有则更新、无则插入」,用 on_conflict(见 insert)。UPDATE 的 SET 里只有 v,没有主键列,即使它们是 Set,这一点见 update 与 delete。
从 JSON 来的 ActiveModel:没有主键就是新行
表单或 API 请求体通常不带主键,主键在 URL 里。直接 from_json 再 save,主键是 NotSet,save 判成 insert:
let am = note::ActiveModel::from_json(json!({ "title": "first, edited again" }))?;
println!("from_json: {am:?}");
let saved = am.save(&db).await?;
println!("returned id: {:?}", saved.id);
--- 3. from_json without id, then save
from_json: ActiveModel { id: NotSet, title: Set("first, edited again"), revision: NotSet }
[hook] before_save insert=true id=NotSet title=Set("first, edited again")
wire> INSERT INTO "note" ("title", "revision") VALUES ($1, $2) RETURNING "id", "title", "revision"
[hook] after_save insert=true model=Model { id: 2, title: "first, edited again", revision: 1 }
wire> INSERT INTO audit (note_id, action) VALUES ($1, $2)
returned id: Unchanged(2)
本意是改 1 号,结果多了一个 2 号,没有任何报错。from_json 的规则是 JSON 里有的键变 Set,没有的变 NotSet(细节见 ActiveValue 与 ActiveModel)。要更新就从库里那一行出发,用 set_from_json:它先把主键列取出来,整体换成 from_json 的结果,再把主键放回去,JSON 里的 id 被忽略。
let mut am = note::Entity::find_by_id(1).one(&db).await?.unwrap().into_active_model();
am.set_from_json(json!({ "id": 999, "title": "via set_from_json" }))?;
println!("after set_from_json: {am:?}");
let saved = am.save(&db).await?;
println!("returned: {saved:?}");
--- 4. load + set_from_json keeps the primary key
wire> SELECT "note"."id", "note"."title", "note"."revision" FROM "note" WHERE "note"."id" = $1 LIMIT $2
after set_from_json: ActiveModel { id: Set(1), title: Set("via set_from_json"), revision: NotSet }
[hook] before_save insert=false id=Set(1) title=Set("via set_from_json")
wire> UPDATE "note" SET "title" = $1 WHERE "note"."id" = $2 RETURNING "id", "title", "revision"
[hook] after_save insert=false model=Model { id: 1, title: "via set_from_json", revision: 2 }
wire> INSERT INTO audit (note_id, action) VALUES ($1, $2)
returned: ActiveModel { id: Unchanged(1), title: Unchanged("via set_from_json"), revision: Unchanged(2) }
两个细节。主键放回去时用的是 self.set(col, v),原来的 Unchanged(1) 变成了 Set(1),对 save 没有影响。JSON 里没有的 revision 变成了 NotSet,而不是保留读出来的 Unchanged(2):set_from_json 换掉的是整个 ActiveModel,只有主键被保留。这次 revision 停在 2,没有被钩子加一,原因在下一节。
四个钩子和一个 new
ActiveModelBehavior 每个实体都必须实现,哪怕是空的 impl ActiveModelBehavior for ActiveModel {}(DeriveActiveModelBehavior 宏生成的就是这一行,expand_derive_active_model_behavior)。五个方法都有默认实现:
#[allow(unused_variables)]
#[async_trait::async_trait]
pub trait ActiveModelBehavior: ActiveModelTrait {
// ...
fn new() -> Self {
<Self as ActiveModelTrait>::default()
}
// ...
async fn before_save<C>(self, db: &C, insert: bool) -> Result<Self, DbErr>
where
C: ConnectionTrait,
{
Ok(self)
}
// ...
async fn after_save<C>(
model: <Self::Entity as EntityTrait>::Model,
db: &C,
insert: bool,
) -> Result<<Self::Entity as EntityTrait>::Model, DbErr>
where
C: ConnectionTrait,
{
Ok(model)
}
// ...
async fn before_delete<C>(self, db: &C) -> Result<Self, DbErr>
// ...
async fn after_delete<C>(self, db: &C) -> Result<Self, DbErr>
// ...
}
| 方法 | 谁调用 | 拿到什么 | 返回值的用处 |
|---|---|---|---|
new | ActiveModel 的 Default(..Default::default())、builder() | 无 | 预填字段的初始 ActiveModel |
before_save | insert、update、update_without_returning(save 经由前两者) | self、db、insert | 交给 INSERT / UPDATE 的就是它返回的 ActiveModel |
after_save | insert、update | RETURNING 拿回的 Model、db、insert | 调用方拿到的 Model |
before_delete | delete | self、db | 交给 DELETE 的 ActiveModel |
after_delete | delete | before_delete 返回值的克隆、db | 被丢弃,只看 Ok / Err |
new 不是钩子,是构造函数。宏生成的 impl Default for ActiveModel 调的是 ActiveModelBehavior::new(),不是全 NotSet 的 ActiveModelTrait::default,所以覆盖 new 就能让 ..Default::default() 带上默认值。
钩子按值收 self、返回 Self,改字段就是改完再交回去。实验里 note 的 before_save 拒绝空标题,并维护 revision;after_save 用同一个 db 往 audit 写一行,标题是 "boom" 时故意报错:
#[async_trait::async_trait]
impl ActiveModelBehavior for ActiveModel {
async fn before_save<C>(mut self, _db: &C, insert: bool) -> Result<Self, DbErr>
where
C: ConnectionTrait,
{
println!(" [hook] before_save insert={insert} id={:?} title={:?}", self.id, self.title);
if let sea_orm::ActiveValue::Set(t) = &self.title {
if t.is_empty() {
return Err(DbErr::Custom("title must not be empty".into()));
}
}
// bump revision: 1 on insert, old+1 on update
self.revision = if insert {
Set(1)
} else {
match self.revision.clone().into_value() {
Some(sea_orm::Value::Int(Some(r))) => Set(r + 1),
_ => sea_orm::ActiveValue::NotSet,
}
};
Ok(self)
}
async fn after_save<C>(model: Model, db: &C, insert: bool) -> Result<Model, DbErr>
where
C: ConnectionTrait,
{
println!(" [hook] after_save insert={insert} model={model:?}");
db.execute_raw(sea_orm::Statement::from_sql_and_values(
db.get_database_backend(),
"INSERT INTO audit (note_id, action) VALUES ($1, $2)",
[model.id.into(), (if insert { "insert" } else { "update" }).into()],
))
.await?;
if model.title == "boom" {
return Err(DbErr::Custom("after_save refused".into()));
}
Ok(model)
}
// before_delete / after_delete only print
}
before_save 只看得到 ActiveModel 里有的东西。上一节 set_from_json 之后 revision 是 NotSet,钩子不知道库里是 2,只能留着 NotSet,UPDATE 里也就没有这一列。
问: 钩子能不能自己去读库里的旧值,比如在 before_save 里查出当前的 revision 再加一?
答: 能,db 就在参数里,但那是一条额外的 SELECT,和随后的 UPDATE 之间没有任何东西把它们绑在一起。在 &DatabaseConnection 上,两条语句各自借一次连接、各自是一个隐式事务(见一次 find 的全过程),两个请求可能读到同一个旧值、写出同一个新值。真要做计数或乐观锁,让数据库算:update_many().col_expr(.., Expr::col(..).add(1)),或者 WHERE revision = 旧值 再看 rows_affected(见 update 与 delete)。
let am = ActiveModelBehavior::before_save(self, db, true).await?;
let model = <Self::Entity as EntityTrait>::insert(am)
.exec_with_returning(db)
.await?;
Self::after_save(model, db, true).await
let am = ActiveModelBehavior::before_delete(self, db).await?;
let am_clone = am.clone();
let delete_res = Self::Entity::delete(am).exec(db).await?;
ActiveModelBehavior::after_delete(am_clone, db).await?;
Ok(delete_res)
update 和 insert 一样,只是传 false。after_delete 拿到的是删除前那个 ActiveModel 的克隆,不是数据库里被删的那一行,DELETE 没有 RETURNING。实验里删 2 号:
--- 8. delete
wire> SELECT "note"."id", "note"."title", "note"."revision" FROM "note" WHERE "note"."id" = $1 LIMIT $2
[hook] before_delete id=Unchanged(2)
wire> DELETE FROM "note" WHERE "note"."id" = $1
[hook] after_delete id=Unchanged(2) title=Unchanged("first, edited again")
rows_affected=1
after_delete 能看到 title,是因为这个 ActiveModel 是从读出来的 Model 转的。只设了主键的 ActiveModel { id: Set(2), ..Default::default() } 去删,after_delete 里其他字段都是 NotSet。
2.0 新加的 update_without_returning 只跑一半:
--- 10. update_without_returning
[hook] before_save insert=false id=Set(3) title=Set("no returning")
wire> UPDATE "note" SET "title" = $1, "revision" = $2 WHERE "note"."id" = $3
rows_affected=1
没有 RETURNING 就没有 Model,after_save 的参数无从谈起,文档注释写明了「does not run ActiveModelBehavior::after_save」。
db,是 0.11 才有的0.10 及以前,四个钩子都是同步函数:before_save(self, insert)、after_save(model, insert)、before_delete(self)、after_delete(self),没有连接参数,钩子里没法查库也没法写库。#1145 的说明写的是「making it possible to mimic database triggers by automatically executing database queries based on other database queries」,做法是「Adds &impl ConnectionTrait to arguments of ActiveModelBehaviour's methods」。接手的 #1328 把整个 trait 改成 async trait,说明里的升级指引是:默认实现「No code changes is required」,覆盖了钩子的要在 impl 上加 #[async_trait::async_trait]。两者首次发布于 0.11.0-rc.1(正式版 0.11.0),CHANGELOG 记为「The ActiveModelBehavior trait becomes async trait」。上面 after_save 往 audit 写的那一行,就是 #1145 想要的「模拟触发器」。
覆盖钩子要写 #[async_trait]
trait 本身声明在 #[async_trait::async_trait] 下面,sea_orm::entity::prelude 也重新导出了 async_trait(prelude.rs),所以上面的 impl 不用额外依赖。去掉 impl 上那行属性,只写 async fn before_save:
// no #[async_trait::async_trait] here
impl ActiveModelBehavior for ActiveModel {
async fn before_save<C>(self, _db: &C, _insert: bool) -> Result<Self, DbErr>
where
C: ConnectionTrait,
{
Ok(self)
}
}
error[E0195]: lifetime parameters or bounds on method `before_save` do not match the trait declaration
--> src/bin/no_async_trait.rs:17:25
|
17 | async fn before_save<C>(self, _db: &C, _insert: bool) -> Result<Self, DbErr>
| ^^^ lifetimes do not match method in trait
|
::: ~/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/sea-orm-2.0.4/src/entity/active_model.rs:1064:1
|
1064 | #[async_trait::async_trait]
| --------------------------- lifetimes in impl do not match this method in trait
async_trait 把 async fn 改写成返回装箱 future 的普通函数,并补上生命周期参数。对加了属性的 impl 跑 cargo expand,before_save 变成这样:
fn before_save<'life0, 'async_trait, C>(
self,
_db: &'life0 C,
insert: bool,
) -> ::core::pin::Pin<
Box<
dyn ::core::future::Future<
Output = Result<Self, DbErr>,
> + ::core::marker::Send + 'async_trait,
>,
>
where
C: ConnectionTrait,
C: 'async_trait,
'life0: 'async_trait,
// ...
trait 那边是同样的改写;impl 不加属性,就是一个原生的 async fn,没有 'life0、'async_trait,签名对不上。trait 文档注释里的示例(impl ActiveModelBehavior for ActiveModel { async fn before_save ... })标了 ignore、没有这行属性,照抄就是这个错误。只写空 impl 时没有方法要对签名,不需要属性。
钩子里报错:什么被回滚了
before_save 返回 Err,? 让 insert 直接返回,INSERT 根本没发:
--- 5. before_save returns Err
[hook] before_save insert=true id=NotSet title=Set("")
result: Err(Custom("title must not be empty"))
after_save 返回 Err 时,INSERT 已经执行完了。ActiveModelTrait::insert 里没有 begin,它收的 db 是 C: ConnectionTrait,在 &DatabaseConnection 上每条语句自动提交。钩子用的又是同一个 db,所以它自己写的 audit 也已经提交了。下面这段在 &db 和 &txn 上各跑一遍同一个会失败的插入:
println!("before: {}", counts(&db, &w).await?);
let r = note::ActiveModel { title: Set("boom".into()), ..Default::default() }.insert(&db).await;
println!("result: {r:?}");
println!("after: {}", counts(&db, &w).await?);
println!("before: {}", counts(&db, &w).await?);
let txn = db.begin().await?;
let r = note::ActiveModel { title: Set("boom".into()), ..Default::default() }.insert(&txn).await;
println!("result: {r:?}");
println!("inside txn: {}", counts(&txn, &w).await?);
txn.rollback().await?;
println!("after rollback: {}", counts(&db, &w).await?);
counts 数 note 和 audit 的行数(它自己的 SELECT 不打印)。
note 有 2 行、audit 有 4 行。insert(&db) 返回 Err(Custom("after_save refused")) 之后,两张表各有几行?平台:M2 Max、macOS、rustc 1.99.0、Postgres 16.14。答案在页末。想让「钩子失败」等于「整次写入失败」,就把写入放进事务:db.begin() 拿到 DatabaseTransaction,传给 insert,钩子拿到的 db 就是这个事务,它发的语句在同一条连接、同一个事务里;出错后 rollback(或者让 txn 被 drop),INSERT 和钩子写的东西一起撤销。事务本身的机制见事务。2.0 的 ActiveModelEx::save 会自己开事务,见 ActiveModelEx。
不走钩子的路径
钩子只在 ActiveModelTrait 的四个方法里被调用。在 2.0.4 的 src/ 和 sea-orm-macros/src/ 里搜 before_save / after_save / before_delete / after_delete,调用点全在 src/entity/active_model.rs:上面四个方法,加上多对多建立关联用的 establish_links(见 ActiveModelEx)。Entity::insert(am).exec(..)、insert_many、update_many、delete_many、delete_by_id 都直接走查询构造器:
let r = note::Entity::insert(note::ActiveModel { title: Set("raw insert".into()), revision: Set(0), ..Default::default() })
.exec(&db)
.await?;
// ... insert_many of two rows, update_many with revision + 10, delete_many, delete_by_id(1)
--- 9. paths that skip hooks
wire> INSERT INTO "note" ("title", "revision") VALUES ($1, $2) RETURNING "id"
Entity::insert(..).exec: last_insert_id=5
wire> INSERT INTO "note" ("title", "revision") VALUES ($1, $2), ($3, $4) RETURNING "id"
insert_many: last_insert_id=Some(7)
wire> UPDATE "note" SET "revision" = "revision" + $1 WHERE "note"."title" LIKE $2
update_many: rows_affected=2
wire> DELETE FROM "note" WHERE "note"."title" = $1
delete_many: rows_affected=1
wire> DELETE FROM "note" WHERE "note"."id" = $1
delete_by_id: rows_affected=1
notes=3 audits=5
一行 [hook] 都没有,audit 还是 5 行。revision 也没被钩子管:Entity::insert 那行写进去的是调用方给的 0。批量方法不走钩子有它的道理:update_many 根本没有 ActiveModel,只有列表达式和 WHERE,钩子的 self 无从构造;insert_many 一条语句插多行,要跑钩子就得逐个调用、改完再拼,after_save 也拿不到逐行的 Model(这里的 RETURNING 只要了 "id")。
| 入口 | before_* | after_* |
|---|---|---|
am.insert(db) / am.update(db) / am.save(db) | 是 | 是 |
am.update_without_returning(db) | 是 | 否 |
am.delete(db) | 是 | 是 |
Entity::insert(am).exec(db)、Entity::update(am).exec(db)、Entity::delete(am).exec(db) | 否 | 否 |
insert_many、update_many、delete_many、delete_by_id | 否 | 否 |
钩子是 ORM 层的约定,不是数据库的约束。必须对每一行成立的规则(非空、唯一、取值范围)放在表上;钩子适合放「通过 ActiveModel 写入时顺手做的事」,并且要知道上表下半部分会绕过它。
动脑筋答案
--- 6. after_save returns Err, no transaction
before: notes=2 audits=4
[hook] before_save insert=true id=NotSet title=Set("boom")
wire> INSERT INTO "note" ("title", "revision") VALUES ($1, $2) RETURNING "id", "title", "revision"
[hook] after_save insert=true model=Model { id: 3, title: "boom", revision: 1 }
wire> INSERT INTO audit (note_id, action) VALUES ($1, $2)
result: Err(Custom("after_save refused"))
after: notes=3 audits=5
--- 7. after_save returns Err, inside a transaction
before: notes=3 audits=5
wire> BEGIN
wire< ReadyForQuery(T)
[hook] before_save insert=true id=NotSet title=Set("boom")
wire> INSERT INTO "note" ("title", "revision") VALUES ($1, $2) RETURNING "id", "title", "revision"
[hook] after_save insert=true model=Model { id: 4, title: "boom", revision: 1 }
wire> INSERT INTO audit (note_id, action) VALUES ($1, $2)
result: Err(Custom("after_save refused"))
inside txn: notes=4 audits=6
wire> ROLLBACK
wire< ReadyForQuery(I)
after rollback: notes=3 audits=5
各多一行:note 3 行、audit 5 行。调用方拿到的是 Err,但 3 号 note 已经在表里,钩子写的 audit 也在。两条 INSERT 之间、Err 返回之前,没有一条 BEGIN,连接一直在 I 状态,每条语句执行完就提交了。
第二段的区别只在 wire> BEGIN 和状态变成 T。同样两条 INSERT、同样的 Err,事务里能看到 4 行和 6 行,ROLLBACK 之后回到 3 和 5。被撤销的 4 号主键不会再被用到:序列的 nextval 不随事务回滚,接下来第 9 步的 Entity::insert 拿到的是 5。
小结
save只看主键:任一主键列是NotSet就insert,否则update;不查库,不看其他列。返回值是into_active_model()之后的全Unchanged,可以接着改接着存。- 复合主键或手填主键的表,
save插不了新行:主键全设上就走 update,表里没有这一行得到RecordNotUpdated。新建用insert,「有则改无则插」用on_conflict。 from_json不带主键时save会插一条新行而不报错;更新要从读出来的行出发,用set_from_json,它保留主键,但其他没出现在 JSON 里的列会变成NotSet。ActiveModelBehavior有new和四个钩子:before_save(self, db, insert)的返回值就是要写的ActiveModel,after_save(model, db, insert)的返回值就是调用方拿到的Model,after_delete拿到的是删除前的克隆。update_without_returning不跑after_save。- trait 用
#[async_trait]声明,覆盖钩子的 impl 也要加,否则E0195。 - 钩子不开事务,用的是调用方给的
db。在&DatabaseConnection上after_save报错时,INSERT和钩子自己写的东西都已经提交;要一起撤销,就在事务里调用。 - 钩子只在
ActiveModelTrait的insert/update/save/delete(以及多对多的关联表写入)里被调用;Entity::insert(..).exec、insert_many、update_many、delete_many、delete_by_id都绕过它。