Rust in Everything
基于 sea-orm 2.0.4

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_manyUpdate::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 上另有一组方法,各自只在某几种状态下动手:

方法NotSetUnchanged(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()NoneSome(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 才能让「赋了同样的值」不算改动。

📝NOTE
没有蠢问题

问: 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)。

📝NOTE
冷知识:1.x 的 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 改不了主键。

❗IMPORTANT
动脑筋:先 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());
平台 M2 Max、macOS、rustc 1.99.0,不连库。

动脑筋答案

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。

💬 关联讨论

发起讨论
加载中...