Rust in Everything
基于 sea-orm 2.0.4

None of the records are updated

主线位置:写入主线的 update 分支。am.update(&db) 用主键做 WHERE、用 Set 的非主键列做 SET,Postgres 上带 RETURNING 一条语句拿回整行;没有返回行就是这个错误。三种执行路径(有列要写、没有列要写、没匹配到行)的完整机制见 update 与 delete,这一页只讲什么时候会看到它、什么时候看不到。

flowchart TB
  am["am.update"] --> setc{"有非主键列是 Set"}
  setc -->|有| upd["UPDATE SET WHERE pk RETURNING"]
  upd -->|0 行| e1["RecordNotUpdated"]
  upd -->|1 行| m1["Model"]
  setc -->|没有| sel["只按主键 SELECT"]
  sel -->|0 行| e2["RecordNotFound"]
  sel -->|1 行| m2["Model,什么都没写"]

本页实验在 M2 Max、macOS、rustc 1.99.0、Postgres 16.14 上运行,sea-orm 2.0.4,以及本站用的 1.1.20,同一份代码。表 item 有三行,id 为 1、2、3。SQL 取自 sqlx 的语句日志(sqlx=info)。

现象

None of the records are updated

这是 DbErr::RecordNotUpdated 的 Display(error.rs:#[error("None of the records are updated")]),不带表名、不带主键。它只说明 UPDATE 的 WHERE 没有匹配到行。

最小复现

src/bin/not_updated.rs,ghost() 是一个内存里造的 id = 99 的 Model:

// 1. one column Set, id 99 does not exist
let mut am = ghost().into_active_model();
am.qty = Set(1);
am.update(&db).await;
// 2. read c3, Set the primary key to 30 and change name
let mut am = c3.into_active_model();
am.id = Set(30);
am.name = Set("cherry v2".into());
am.update(&db).await;
// 3. nothing Set, id 99 does not exist
ghost().into_active_model().update(&db).await;
// 4. nothing Set, id 2 exists
b2.into_active_model().update(&db).await;

2.0.4 的输出(第 2、4 段读行的 SELECT 删掉了,标为 ...):

--- 1. one column Set, id 99 does not exist
summary="UPDATE \"item\" SET \"qty\" …" db.statement="\n\nUPDATE \"item\" SET \"qty\" = $1 WHERE \"item\".\"id\" = $2 RETURNING \"id\", \"sku\", \"name\", \"qty\", \"note\"\n" rows_affected=0 rows_returned=0 elapsed=1.657417ms elapsed_secs=0.001657417
=> Err RecordNotUpdated
   Display: None of the records are updated
--- 2. read c3, Set the primary key to 30 and change name
...
summary="UPDATE \"item\" SET \"name\" …" db.statement="\n\nUPDATE \"item\" SET \"name\" = $1 WHERE \"item\".\"id\" = $2 RETURNING \"id\", \"sku\", \"name\", \"qty\", \"note\"\n" rows_affected=0 rows_returned=0 elapsed=348.291µs elapsed_secs=0.000348291
=> Err RecordNotUpdated
   Display: None of the records are updated
--- 3. nothing Set, id 99 does not exist
summary="SELECT \"item\".\"id\", \"item\".\"sku\", \"item\".\"name\", …" db.statement="\n\nSELECT \"item\".\"id\", \"item\".\"sku\", \"item\".\"name\", \"item\".\"qty\", \"item\".\"note\" FROM \"item\" WHERE \"item\".\"id\" = $1 LIMIT $2\n" rows_affected=0 rows_returned=0 elapsed=174.833µs elapsed_secs=0.000174833
=> Err RecordNotFound("Failed to find updated item")
   Display: RecordNotFound Error: Failed to find updated item
--- 4. nothing Set, id 2 exists
...
summary="SELECT \"item\".\"id\", \"item\".\"sku\", \"item\".\"name\", …" db.statement="\n\nSELECT \"item\".\"id\", \"item\".\"sku\", \"item\".\"name\", \"item\".\"qty\", \"item\".\"note\" FROM \"item\" WHERE \"item\".\"id\" = $1 LIMIT $2\n" rows_affected=1 rows_returned=1 elapsed=136.042µs elapsed_secs=0.000136042
=> Ok Model { id: 2, sku: "b2", name: "banana", qty: 5, note: None }

1.1.20 上四段的语句和结果逐条相同。

情况发出的语句结果
至少一列 Set,主键对应的行不存在(被删了、id 写错了)UPDATE ... RETURNING,0 行RecordNotUpdated
读出来的行把主键 Set 成新值,再改别的列UPDATE ... SET "name" = $1 WHERE "id" = $2,$2 是新值 30RecordNotUpdated
一列都没 Set,行不存在只有 SELECTRecordNotFound("Failed to find updated item")
一列都没 Set,行存在只有 SELECTOk,什么都没写

第 2 行最容易误读:SET 里没有 "id",sea-orm 不是「改主键失败」,而是拿新主键去找一行不存在的记录。主键改成一个已存在的 id 时更糟:不报错,更新的是那一行,原来那一行不动。src/bin/pk_existing.rs 读出 3 号、把 id 设成 2、改 name(2.0.4):

=> Ok(Model { id: 2, sku: "b2", name: "cherry v2", qty: 5, note: None })
  Model { id: 1, sku: "a1", name: "apple", qty: 3, note: None }
  Model { id: 2, sku: "b2", name: "cherry v2", qty: 5, note: None }
  Model { id: 3, sku: "c3", name: "cherry", qty: 7, note: None }

2 号 banana 的名字被改成了 cherry v2,3 号原样;1.1.20 输出相同。

出处

Update::one 先扫主键列,Set 或 Unchanged 都变成 WHERE pk = $n;再扫其他列,只有 Set 的进 SET,主键列一律跳过。ActiveValue 一个字段只存一个值,Unchanged(3) 被 Set(30) 覆盖后,3 就找不回来了。

执行在 Updater::exec_update_and_return_updated:is_noop(SET 为空)时改调 find_updated_model_by_id,查不到报 RecordNotFound("Failed to find updated item");否则带 RETURNING 执行,None 映射成 DbErr::RecordNotUpdated。

📝NOTE
冷知识:它原来就叫 RecordNotFound

0.11.0 之前,UPDATE 没匹配到行报的是 DbErr::RecordNotFound("None of the database rows are affected")。PR #1425(「RecordNotFound -> RecordNotUpdated」,0.11.0)把它拆成单独的分支,理由只有一句:「This makes the behaviour consistent with RecordNotInserted」。CHANGELOG 里写成「DbErr::RecordNotFound("None of the database rows are affected") is moved to a dedicated error variant DbErr::RecordNotUpdated」。同一个版本里的 PR #1384(「Noop when update without providing any values」)加了 is_noop 那条路,它查不到行时用的仍是 RecordNotFound,于是「行不存在」从 0.11.0 起就有两种报法,取决于有没有列要写。

修法

「行不存在」当成 404:两个错误一起认。

match am.update(&db).await {
  Ok(m) => println!("updated {}", m.id),
  Err(DbErr::RecordNotUpdated) | Err(DbErr::RecordNotFound(_)) => println!("404: item 99 not found"),
  Err(e) => return Err(e),
}
--- fix: treat both errors as "not found"
summary="UPDATE \"item\" SET \"qty\" …" db.statement="\n\nUPDATE \"item\" SET \"qty\" = $1 WHERE \"item\".\"id\" = $2 RETURNING \"id\", \"sku\", \"name\", \"qty\", \"note\"\n" rows_affected=0 rows_returned=0 elapsed=152µs elapsed_secs=0.000152
404: item 99 not found

RecordNotFound 还会来自别处(例如 exec_with_returning 找不到插入的行),这里的 match 只包住 update 一次调用,不会混淆。

改主键用 update_many,新旧两个值都由调用方给出:

let r = item::Entity::update_many()
  .col_expr(item::Column::Id, Expr::value(30))
  .filter(item::Column::Id.eq(3))
  .exec(&db)
  .await?;
--- fix: change the primary key with update_many
summary="UPDATE \"item\" SET \"id\" …" db.statement="\n\nUPDATE \"item\" SET \"id\" = $1 WHERE \"item\".\"id\" = $2\n" rows_affected=1 rows_returned=0 elapsed=431.042µs elapsed_secs=0.000431042
=> rows_affected=1

update_many 不检查影响行数,0 行时是 Ok(UpdateResult { rows_affected: 0 }),要自己看。

本意是「有就改、没有就插」时,不要靠 update 失败再 insert,用 on_conflict(..).update_columns(..) 一条语句完成,见 insert。

怎么确认没中招

  • 搜 RecordNotUpdated:每一处匹配旁边都应该同时处理 RecordNotFound,否则「一列都没改」的请求打到不存在的 id 上会走进通用的错误分支。
  • 搜对主键字段的赋值(am.id = Set():后面跟的是 update / save 时,就是表里第 2 行的情形。
  • 本站目前不通过 ActiveModel::update 写库,更新都用 update_many,不会看到这个错误;但 rows_affected == 0 也不会报错,要看调用方有没有检查。

小结

  • RecordNotUpdated(None of the records are updated)只在至少一列 Set、WHERE 主键没匹配时出现。
  • 把主键 Set 成新值不会改主键,新值进了 WHERE,多半得到这个错误。
  • 一列都没 Set 时不发 UPDATE:行存在就 Ok,不存在是 RecordNotFound("Failed to find updated item")。
  • 2.0.4 和 1.1.20 行为相同;处理「不存在」时两个错误都要认,改主键用 update_many。

💬 关联讨论

发起讨论
加载中...