ActiveValue 与 ActiveModel
主线位置:写入主线的起点。
cake::ActiveModel { name: Set(..), ..Default::default() }.insert(&db)里,ActiveModel的每个字段都是一个ActiveValue,Insert::one、Update::one逐列看它是哪一态,决定这一列进不进 SQL。这一页讲三态本身、改变状态的方法、宏生成的ActiveModelTrait,以及from_json怎么把一个 JSON 对象变成三态。三态落到INSERT/UPDATE的细节在 insert 和 update 与 delete。
stateDiagram-v2 state "NotSet 不写" as NotSet state "Unchanged 读来没改" as Unchanged state "Set 要写" as Set [*] --> NotSet: Default [*] --> Unchanged: into_active_model NotSet --> Set: Set 或 set_ne Unchanged --> Set: Set 或 reset Unchanged --> Set: set_ne 新值不同 Unchanged --> Unchanged: set_ne 新值相同 Set --> NotSet: take 或 not_set Unchanged --> NotSet: take 或 not_set
实验用一张 item 表:id serial primary key、sku text not null unique、name text not null、qty int not null default 0、note text,实体是 dense 写法,Model 另外 derive 了 Serialize / Deserialize(from_json 要用)。平台 M2 Max、macOS、rustc 1.99.0、Postgres 16.14,依赖 sea-orm = "=2.0.4"(sqlx-postgres、runtime-tokio-rustls、macros、with-json),表在自己的 schema 里。本页的程序大多不连库。
三态:一个枚举
ActiveValue<V> 就是三个分支:
#[derive(Clone, Debug)]
pub enum ActiveValue<V>
where
V: Into<Value>,
{
Set(V),
Unchanged(V),
NotSet,
}
V: Into<Value> 是唯一的约束:每个字段的值最后都要变成 sea-query 的 Value(见列与值)才能当参数绑出去。Set 和 Unchanged 都带着值,区别只是一个标记。这个标记在三条写入路径上的意义不同:
| 状态 | Insert::one / insert_many | Update::one 的非主键列 | Update::one / Delete::one 的主键列 |
|---|---|---|---|
Set(v) | 写进列表和 VALUES | 写进 SET | 进 WHERE |
Unchanged(v) | 也写进列表和 VALUES | 跳过 | 进 WHERE |
NotSet | 跳过,交给列的默认值 | 跳过 | 报 PrimaryKeyNotSet |
只有 UPDATE 区分 Set 和 Unchanged;INSERT 把两者一视同仁(Insert::one 里是 ActiveValue::Set(value) | ActiveValue::Unchanged(value) =>),所以拿一个读出来的 Model 直接 into_active_model().insert(..),主键也会写进去,见 insert。NotSet 和 Set(None) 也不一样:前者这一列不出现在 SQL 里,后者写一个 NULL。
ActiveValue 的 PartialEq 比较分支和值,Set(3) == Unchanged(3) 是 false。
改变状态的方法
直接赋值 am.qty = Set(4) 是最常用的写法。ActiveValue 上另有一组方法,各自只在某几种状态下动手:
| 方法 | NotSet | Unchanged(old) | Set(old) |
|---|---|---|---|
set_ne(v) / set_if_not_equals(v) | Set(v) | v == old 时不变,否则 Set(v) | Set(v) |
reset() | 不变 | Set(old) | 不变 |
set_if_unset(v) | Set(v) | 不变 | 不变 |
take() | 返回 None | 返回 Some(old),自己变成 NotSet | 同左 |
into_value() | None | Some(old.into()) | 同左 |
set_ne 的全部逻辑是一个 match:
pub fn set_ne(&mut self, value: V)
where
V: PartialEq,
{
match self {
ActiveValue::Unchanged(current) if &value == current => {}
_ => *self = ActiveValue::Set(value),
}
}
只有 Unchanged 分支会和新值比较。Set 分支不比较,因为 Set(old) 里的 old 已经不是数据库里的值了,和它相等说明不了「没改」。set_if_not_equals 是 set_ne 的旧名字,现在只是转一下调用。reset 是反方向的操作:
pub fn reset(&mut self) {
*self = match self.take() {
Some(value) => ActiveValue::Set(value),
None => ActiveValue::NotSet,
};
}
它把读来的值标成「要写」,用处是强制 update 把这一列写回去;ActiveModelTrait::reset_all 对每一列调一次。
实验:每个方法把状态变成什么
逐个调用、打印结果,最后一组看 ActiveModelTrait::is_changed(src/bin/av_states.rs,摘录):
let mut a: ActiveValue<i32> = Unchanged(3);
a.set_ne(3);
println!("Unchanged(3).set_ne(3) -> {a:?}");
// ... 其余每行同样的写法
let m = item::Model { id: 1, sku: "a1".into(), name: "apple".into(), qty: 3, note: None };
let mut am = m.clone().into_active_model();
println!("into_active_model -> {am:?}");
println!("is_changed -> {}", am.is_changed());
am.name.set_ne("apple".to_owned());
println!("after set_ne(same) -> is_changed {}", am.is_changed());
am.name = Set("apple".to_owned());
println!("after Set(same) -> is_changed {}", am.is_changed());
let am = m.into_active_model().reset_all();
println!("reset_all -> {am:?}");
println!("Default -> {:?}", <item::ActiveModel as Default>::default());
Unchanged(3).set_ne(3) -> Unchanged(3)
Unchanged(3).set_ne(4) -> Set(4)
Set(3).set_ne(3) -> Set(3)
NotSet.set_ne(3) -> Set(3)
Unchanged(3).reset() -> Set(3)
NotSet.reset() -> NotSet
Set(3).take() -> Some(3), left NotSet
Unchanged(3).into_value -> Some(Int(Some(3)))
Set(3) == Unchanged(3) -> false
into_active_model -> ActiveModel { id: Unchanged(1), sku: Unchanged("a1"), name: Unchanged("apple"), qty: Unchanged(3), note: Unchanged(None) }
is_changed -> false
after set_ne(same) -> is_changed false
after Set(same) -> is_changed true
reset_all -> ActiveModel { id: Set(1), sku: Set("a1"), name: Set("apple"), qty: Set(3), note: Set(None) }
Default -> ActiveModel { id: NotSet, sku: NotSet, name: NotSet, qty: NotSet, note: NotSet }
is_changed 只看有没有一列是 Set,不看值。把读来的值原样 Set 回去,is_changed 就是 true,update 也会把这一列写进 SET;用 set_ne 才能让「赋了同样的值」不算改动。
问: Set 为什么不顺便记住旧值?那样 is_changed 和 update 都能自己判断值到底变没变,用不着 set_ne。
答: 因为 ActiveValue 是每列一个、三态之一的枚举,Set(V) 只装得下一个值。into_active_model 之后,旧值只存在 Unchanged(old) 里;一旦赋成 Set(new),旧值就被覆盖了,之后谁也比不了。set_ne 是在覆盖之前、旧值还在的那一刻比较。这样一个字段的大小就是 V 加一个标记,代价是「值没变」这件事只能在赋值时判断,而且要调用方记得用 set_ne。
ActiveModel 与 ActiveModelTrait
DeriveEntityModel 为 Model 的每个字段生成一个 ActiveValue 字段(见 DeriveEntityModel 与 dense 格式),并在 impl_active_model_convert 里生成 From<Model>:每个字段包成 Unchanged。into_active_model() 就是这个 From。
ActiveModelTrait 是按列名(Column 枚举)动态访问字段的接口,Insert::one、Update::one、from_json 都只通过它读写 ActiveModel。宏在 impl_active_model_trait_methods 里为每个方法生成一个按列分派的 match:
fn get(&self, c: <Self::Entity as sea_orm::EntityTrait>::Column) -> sea_orm::ActiveValue<sea_orm::Value> {
match c {
#(<Self::Entity as sea_orm::EntityTrait>::Column::#names => self.#fields.clone().into_wrapped_value(),)*
_ => sea_orm::ActiveValue::NotSet,
}
}
// ...
fn try_set(&mut self, c: <Self::Entity as sea_orm::EntityTrait>::Column, v: sea_orm::Value) -> Result<(), sea_orm::DbErr> {
match c {
#(<Self::Entity as sea_orm::EntityTrait>::Column::#names => self.#fields = sea_orm::ActiveValue::Set(sea_orm::sea_query::ValueType::try_from(v).map_err(|e| sea_orm::DbErr::Type(e.to_string()))?),)*
_ => return Err(sea_orm::DbErr::Type(format!("ActiveModel does not have this field: {:?}", sea_orm::ColumnTrait::as_column_ref(&c)))),
}
Ok(())
}
get 返回的是 ActiveValue<Value>:字段的具体类型被抹成 Value,状态保留。get 会 clone 一次值,Insert::one 用的是不复制的 take。try_set 反过来用 ValueType::try_from 把 Value 转回字段类型,类型不对就是 DbErr::Type;set 是 try_set 加一个 panic。动态访问的结果(src/bin/av_dyn.rs):
get(Qty) -> Unchanged(Int(Some(3)))
set(Qty, 4) -> Set(4)
try_set(Qty, "x") -> Err(Type("Value type mismatch"))
not_set(Note) -> NotSet
trait 里还有一个 fn default() -> Self,宏生成的实现是全部 NotSet;而 std::default::Default 的实现转给 ActiveModelBehavior::new(),后者默认又回到 ActiveModelTrait::default()。两个 default 同名,use sea_orm::ActiveModelTrait 之后写 item::ActiveModel::default() 编译不过:
error[E0034]: multiple applicable items in scope
--> src/bin/av_states.rs:44:66
|
44 | println!("Default -> {:?}", item::ActiveModel::default());
| ^^^^^^^ multiple `default` found
|
= note: candidate #1 is defined in an impl of the trait `ActiveModelTrait` for the type `ActiveModel`
= note: candidate #2 is defined in an impl of the trait `std::default::Default` for the type `ActiveModel`
结构体更新语法 ..Default::default() 没有这个问题,它只找 Default。覆盖了 ActiveModelBehavior::new() 的实体,两个 default 的结果也不同:Default 带着 new() 里预填的值,ActiveModelTrait::default() 全是 NotSet。new() 和其他钩子在 save 与 ActiveModelBehavior。
from_json:键在不在决定 Set 还是 NotSet
ActiveModelTrait::from_json(with-json feature)要解决的问题是:表单或 API 传来的 JSON 往往只有几个字段,而 Model 的每个非 Option 字段在 serde 看来都是必填的。它的做法是先补齐、再反序列化、最后按原来的键改状态:
let dummy_am = Self::default_values();
let len = <<Self::Entity as EntityTrait>::Column>::iter().len();
// Mark down which attribute exists in the JSON object
let mut json_keys = Vec::with_capacity(len);
let mut merged = serde_json::Map::with_capacity(len);
for col in <<Self::Entity as EntityTrait>::Column>::iter() {
let key = col.json_key();
let has_key = input.contains_key(key);
json_keys.push((col, has_key));
match dummy_am.get(col) {
ActiveValue::Unchanged(value) | ActiveValue::Set(value) => {
merged.insert(key.to_owned(), sea_query::sea_value_to_json_value(&value));
}
_ => {}
}
}
merged.append(&mut input);
// ...
let model: <Self::Entity as EntityTrait>::Model =
serde_json::from_value(json_value).map_err(json_err)?;
let mut am = model.into_active_model();
// Transform attribute that exists in JSON object into ActiveValue::Set, otherwise ActiveValue::NotSet
for (col, json_key_exists) in json_keys {
match (json_key_exists, am.get(col)) {
(true, ActiveValue::Set(value) | ActiveValue::Unchanged(value)) => {
am.set(col, value);
}
_ => {
am.not_set(col);
}
}
}
default_values() 由宏生成,给每个字段一个占位值:Option<T> 字段是 Set(None),其他字段用 sea-query 的 Value::dummy_value()(整数是 0、字符串是空串)。merged 先放占位值,再用 append 把输入的键值盖上去,于是 serde_json::from_value 总能凑出一个完整的 Model,serde 的 #[serde(rename)] 之类的属性也照常生效。最后一步只看「键在不在原始 JSON 里」:在就 Set,不在就 NotSet,占位值一个也留不下来。实验(src/bin/av_states.rs 的后半):
for j in [
json!({"name": "pear"}),
json!({"id": 9, "sku": "p9", "name": "pear", "qty": 1, "note": null}),
json!({"note": null}),
json!({"qty": null}),
json!({"qty": "many"}),
json!(["not", "an", "object"]),
] {
match item::ActiveModel::from_json(j.clone()) {
Ok(am) => println!("from_json {j} -> {am:?}"),
Err(e) => println!("from_json {j} -> Err {e:?} / {e}"),
}
}
from_json {"name":"pear"} -> ActiveModel { id: NotSet, sku: NotSet, name: Set("pear"), qty: NotSet, note: NotSet }
from_json {"id":9,"name":"pear","note":null,"qty":1,"sku":"p9"} -> ActiveModel { id: Set(9), sku: Set("p9"), name: Set("pear"), qty: Set(1), note: Set(None) }
from_json {"note":null} -> ActiveModel { id: NotSet, sku: NotSet, name: NotSet, qty: NotSet, note: Set(None) }
from_json {"qty":null} -> Err Json("invalid type: null, expected i32") / Json Error: invalid type: null, expected i32
from_json {"qty":"many"} -> Err Json("invalid type: string \"many\", expected i32") / Json Error: invalid type: string "many", expected i32
from_json ["not","an","object"] -> Err Json("invalid type: expected JSON object for item") / Json Error: invalid type: expected JSON object for item
几点从输出里能读出来:"note": null 是 Set(None),会把这一列写成 NULL,和不传这个键(NotSet)不同;非 Option 字段传 null 或者类型不对,报的是 serde 的原文(invalid type: ...,见 serde 卷的 invalid type),包在 DbErr::Json 里;JSON 里带了 id,主键就是 Set,之后 save 会把它当成更新(见 save 与 ActiveModelBehavior)。
from_json 缺一个字段就报错本站用的 1.1.20 里,from_json 直接把输入交给 serde_json::from_value,没有补占位值这一步。同一个实体、同样的两个输入,在 1.1.20 下(exp-seaorm-write1-oldjson):
from_json {"name":"pear"} -> Err Json("missing field `id`")
from_json {"id":9,"name":"pear","note":null,"qty":1,"sku":"p9"} -> ActiveModel { id: Set(9), sku: Set("p9"), name: Set("pear"), qty: Set(1), note: Set(None) }
也就是说,1.x 里「哪些键在 JSON 里就 Set 哪些」只对字段齐全的 JSON 有意义,部分更新只能把非 Option 字段都传上。PR #2599(「Improve ActiveModel::from_json」)说明:「will make ActiveModel::from_json more useful, because it will not error out of missing fields, and it still respects any serde(_) directive on the Model」。代价是 trait 约束多了 TryIntoModel 和 Serialize,CHANGELOG 的 2.0.0 一节把它列为破坏性改动。仓库里最早包含这个提交的 tag 是 2.0.0-rc.28(更早的候选版本没有 tag)。
已有的 ActiveModel 要合并一份 JSON,用 set_from_json:它先把主键列 take 出来,from_json 之后再放回去,所以 JSON 改不了主键。
Set 一个和原来相同的值,再 set_ne 同一个值,is_changed() 是什么?let m = item::Model { id: 1, sku: "a1".into(), name: "apple".into(), qty: 3, note: None };
let mut am = m.into_active_model();
am.name = Set("apple".to_owned());
am.name.set_ne("apple".to_owned());
println!("puzzle: {:?}, is_changed {}", am.name, am.is_changed());
动脑筋答案
puzzle: Set("apple"), is_changed true
set_ne 只在当前是 Unchanged 时才比较。第一行的 Set 已经把 Unchanged("apple") 换成了 Set("apple"),旧值不在了;set_ne 走 _ => 分支,再写一次 Set("apple")。set_ne 撤销不了之前的 Set,要恢复成「没改」只能重新从 Model 转一次,或者手动写回 Unchanged。
小结
ActiveValue是Set/Unchanged/NotSet三个分支的枚举;INSERT写Set和Unchanged,UPDATE只SET被Set的非主键列,主键Set或Unchanged都进WHERE。NotSet是这一列不进 SQL,Set(None)是写 NULL。set_ne只在新值等于Unchanged的旧值时保持Unchanged;reset/reset_all把Unchanged变成Set;is_changed只看有没有Set。into_active_model()是宏生成的From<Model>,全部Unchanged;ActiveModelTrait按Column动态读写,值抹成Value。- 两个
default:Default走ActiveModelBehavior::new(),ActiveModelTrait::default()全NotSet,同时在作用域里时要写全路径。 from_json先用占位值补齐、再反序列化,最后按键在不在给Set或NotSet;1.1.20 缺字段就报missing field。