Rust in Everything
基于 sea-orm 2.0.4

None of the records are inserted

主线位置:写入主线的 insert 分支,RETURNING 之后。Postgres 上 Insert::exec 发 INSERT ... RETURNING "id",从返回的行里取主键;ON CONFLICT DO NOTHING 撞上冲突时一行也不返回,sea-orm 只好报错。各个 exec 的区别见 insert 的「四个 exec,三种 RETURNING」和「on_conflict:upsert 与 DO NOTHING」,这一页把「按设计什么都不做」被当成错误的几种样子放在一起。

flowchart TB
  ins["INSERT ON CONFLICT DO NOTHING"] --> rows{"返回了行"}
  rows -->|有| okr["Ok"]
  rows -->|没有| which{"哪个 exec"}
  which -->|"Insert exec"| e1[RecordNotInserted]
  which -->|"exec_with_returning"| e2["RecordNotFound"]
  which -->|"exec_without_returning"| zero["Ok 0"]
  which -->|"TryInsert exec"| conf["Ok Conflicted"]

本页实验在 M2 Max、macOS、rustc 1.99.0、Postgres 16.14 上运行,sea-orm 2.0.4,以及本站用的 1.1.20。表 item 有 id serial 主键和 sku text UNIQUE,a1 已经存在。SQL 取自 sqlx 的语句日志(sqlx=info)。

现象

一个「已存在就跳过」的插入,第二次调用返回错误:

None of the records are inserted

这是 DbErr::RecordNotInserted 的 Display(error.rs 里的 #[error("None of the records are inserted")])。换成 exec_with_returning 是另一个错误:RecordNotFound("Failed to find inserted item")。

最小复现

src/bin/on_conflict.rs,每段用同一个 ActiveModel(sku = "a1"):

let nothing = OnConflict::column(item::Column::Sku).do_nothing().to_owned();
// 1.
item::Entity::insert(am("a1")).on_conflict(nothing.clone()).exec(&db).await;
// 2.
item::Entity::insert(am("a1")).on_conflict(nothing.clone()).exec_with_returning(&db).await;
// 3.
item::Entity::insert(am("a1")).on_conflict(nothing.clone()).exec_without_returning(&db).await;
// 4.
item::Entity::insert(am("a1")).on_conflict_do_nothing_on([item::Column::Sku]).exec(&db).await;
// 5.
item::Entity::insert(am("a1")).on_conflict_do_nothing_on([item::Column::Sku]).exec_with_returning(&db).await;
// 6.
item::Entity::insert(am("a1")).on_conflict_do_nothing().exec(&db).await;

2.0.4 的输出(第 2 段起的 sqlx 日志只留 SQL 不同的几行):

--- 1. on_conflict(..do_nothing()), Insert::exec
summary="INSERT INTO \"item\" (\"sku\", …" db.statement="\n\nINSERT INTO \"item\" (\"sku\", \"name\") VALUES ($1, $2) ON CONFLICT (\"sku\") DO NOTHING RETURNING \"id\"\n" rows_affected=0 rows_returned=0 elapsed=1.52675ms elapsed_secs=0.00152675
=> Err(RecordNotInserted)
   Display: None of the records are inserted
--- 2. on_conflict(..do_nothing()), Insert::exec_with_returning
summary="INSERT INTO \"item\" (\"sku\", …" db.statement="\n\nINSERT INTO \"item\" (\"sku\", \"name\") VALUES ($1, $2) ON CONFLICT (\"sku\") DO NOTHING RETURNING \"id\", \"sku\", \"name\", \"qty\", \"note\"\n" rows_affected=0 rows_returned=0 elapsed=335.041µs elapsed_secs=0.000335041
=> Err(RecordNotFound("Failed to find inserted item"))
--- 3. on_conflict(..do_nothing()), Insert::exec_without_returning
summary="INSERT INTO \"item\" (\"sku\", …" db.statement="\n\nINSERT INTO \"item\" (\"sku\", \"name\") VALUES ($1, $2) ON CONFLICT (\"sku\") DO NOTHING\n" rows_affected=0 rows_returned=0 elapsed=306.708µs elapsed_secs=0.000306708
=> Ok(0)
--- 4. on_conflict_do_nothing_on([Sku]), TryInsert::exec
...
=> Ok(Conflicted)
--- 5. on_conflict_do_nothing_on([Sku]), TryInsert::exec_with_returning
...
=> Err(RecordNotFound("Failed to find inserted item"))
--- 6. on_conflict_do_nothing() targets the primary key, sku clashes
summary="INSERT INTO \"item\" (\"sku\", …" db.statement="\n\nINSERT INTO \"item\" (\"sku\", \"name\") VALUES ($1, $2) ON CONFLICT (\"id\") DO NOTHING RETURNING \"id\"\n" rows_affected=0 rows_returned=0 elapsed=408.625µs elapsed_secs=0.000408625
=> Err Query Error: error returned from database: duplicate key value violates unique constraint "item_sku_key" at line 666

第 1、4 段发的是同一条 SQL,结果一个是错误、一个是 Ok(Conflicted)。第 5 段用了 TryInsert 也没用。第 6 段的冲突目标是 "id",sku 撞了照样是 23505。

1.1.20 没有 on_conflict_do_nothing_on,TryInsert 用 .on_conflict(..).do_nothing() 拿到,六段的结果与 2.0.4 逐条相同(sqlx 日志行删掉了):

--- 1. on_conflict(..do_nothing()), Insert::exec
=> Err(RecordNotInserted)
   Display: None of the records are inserted
--- 2. on_conflict(..do_nothing()), Insert::exec_with_returning
=> Err(RecordNotFound("Failed to find inserted item"))
--- 3. on_conflict(..do_nothing()), Insert::exec_without_returning
=> Ok(0)
--- 4. on_conflict(..).do_nothing(), TryInsert::exec
=> Ok(Conflicted)
--- 5. on_conflict(..).do_nothing(), TryInsert::exec_with_returning
=> Err(RecordNotFound("Failed to find inserted item"))
--- 6. on_conflict_do_nothing() targets the primary key, sku clashes
=> Err Query Error: error returned from database: duplicate key value violates unique constraint "item_sku_key"

出处

  • exec_insert:支持 RETURNING 时 query_all 后 rows.pop(),没有行就 return Err(DbErr::RecordNotInserted)。它不知道语句里有没有 DO NOTHING,「冲突」和「真的没插进去」对它是一回事。
  • exec_insert_with_returning:.one(db) 拿到 None 时报 RecordNotFound("Failed to find inserted item")。
  • TryInsert::exec 把 RecordNotInserted 翻译成 TryInsertResult::Conflicted;TryInsert::exec_with_returning 翻译的也是 RecordNotInserted,可它调的 exec_insert_with_returning 报的是 RecordNotFound,所以 Conflicted 在这条路上永远出不来。
  • exec_insert_without_returning 只返回 rows_affected,冲突时是 0,不报错。
  • Insert::on_conflict_do_nothing 的冲突目标是实体的主键列;on_conflict_do_nothing_on 才能指定唯一列。主键是 serial、插入时不带主键的表,前者几乎不可能冲突。

exec_with_returning 这条在上游是 issue #2389(「postgres, RecordNotFound Error: Failed to find inserted item」,insert_many(..).on_conflict(..do_nothing()).exec_with_returning),2025-12 以「The postgres issue should have been solved in 2.0」关闭,同时加了一个测试(changelog/2.0.0-rc.23.md:「Added test coverage and fixes for insert_many with on_conflict_do_nothing and returning clauses」)。2.0.4 上实测(src/bin/on_conflict_many.rs):只有不经过 TryInsert 的那条路修好了。

--- insert_many([a1]).on_conflict(..do_nothing()).exec_with_returning
summary="INSERT INTO \"item\" (\"sku\", …" db.statement="\n\nINSERT INTO \"item\" (\"sku\", \"name\") VALUES ($1, $2) ON CONFLICT (\"sku\") DO NOTHING RETURNING \"id\", \"sku\", \"name\", \"qty\", \"note\"\n" rows_affected=0 rows_returned=0 elapsed=2.3465ms elapsed_secs=0.0023465
=> Ok([])
--- insert_many([a1]).on_conflict_do_nothing_on([Sku]).exec_with_returning
summary="INSERT INTO \"item\" (\"sku\", …" db.statement="\n\nINSERT INTO \"item\" (\"sku\", \"name\") VALUES ($1, $2) ON CONFLICT (\"sku\") DO NOTHING RETURNING \"id\", \"sku\", \"name\", \"qty\", \"note\"\n" rows_affected=0 rows_returned=0 elapsed=269.458µs elapsed_secs=0.000269458
=> Err(RecordNotFound("Failed to find inserted item"))
📝NOTE
冷知识:最早是一次 unwrap panic

issue #1175(0.10.1)报的是同一个场景:「Running the following query twice with the same value will yield no return result in the second run」,结果是 called Option::unwrap() on a None value,panic 在 exec_insert 取返回行的地方。它被并进 issue #899,由 PR #1021 修掉(0.11.0),PR 写的是「throw DbErr::RecordNotInserted if none of the records are being inserted」:panic 变成了错误,就是本页标题。「什么都不做」仍然是错误,于是 PR #1712(0.12.1,CHANGELOG:「Insert on conflict do nothing to return Ok」)给 TryInsert 加了 Conflicted,旧的 Insert::exec 行为保持不变。到 2.0.4,这两条路并存。

修法

要区分「插入了」和「已存在」:用 TryInsert,按结果分支。 2.0 用 on_conflict_do_nothing_on([唯一列]),1.1.20 用 .on_conflict(..).do_nothing():

for sku in ["a1", "z9"] {
  match item::Entity::insert(am(sku)).on_conflict_do_nothing_on([item::Column::Sku]).exec(&db).await? {
    TryInsertResult::Inserted(r) => println!("{sku}: inserted, id {}", r.last_insert_id),
    TryInsertResult::Conflicted => println!("{sku}: already there"),
    TryInsertResult::Empty => println!("{sku}: nothing to insert"),
  }
}
--- fix: match on TryInsertResult
summary="INSERT INTO \"item\" (\"sku\", …" db.statement="\n\nINSERT INTO \"item\" (\"sku\", \"name\") VALUES ($1, $2) ON CONFLICT (\"sku\") DO NOTHING RETURNING \"id\"\n" rows_affected=0 rows_returned=0 elapsed=151.667µs elapsed_secs=0.000151667
a1: already there
summary="INSERT INTO \"item\" (\"sku\", …" db.statement="\n\nINSERT INTO \"item\" (\"sku\", \"name\") VALUES ($1, $2) ON CONFLICT (\"sku\") DO NOTHING RETURNING \"id\"\n" rows_affected=1 rows_returned=1 elapsed=310.916µs elapsed_secs=0.000310916
z9: inserted, id 11

1.1.20 上输出相同。

只要幂等、不关心插没插:exec_without_returning,冲突时 Ok(0),不报错。

要整行:不要用单行的 exec_with_returning。 2.0 的 InsertMany::exec_with_returning(不经过 TryInsert)冲突时是 Ok([]);或者 TryInsert::exec 拿到 Conflicted 后再按唯一列查一次。

怎么确认没中招

  • 搜 do_nothing,看它后面接的是哪个 exec:Insert::exec 和任何 exec_with_returning 在冲突时都返回 Err。本站的 grant_entitlement_internal 在购买时用 do_nothing(),配的是 exec_without_returning,重复购买得到 Ok(0),不受影响。
  • 搜 on_conflict_do_nothing()(不带 _on):冲突目标是主键,确认这正是你要的;按业务唯一键去重要写 on_conflict_do_nothing_on。
  • 匹配错误时不要只认 RecordNotInserted:同一个冲突在 exec_with_returning 上是 RecordNotFound。

小结

  • ON CONFLICT DO NOTHING 冲突时 Postgres 不返回行;Insert::exec 报 RecordNotInserted(None of the records are inserted),exec_with_returning(包括 TryInsert 的)报 RecordNotFound("Failed to find inserted item")。
  • 只有 TryInsert::exec 给 Ok(Conflicted),exec_without_returning 给 Ok(0);2.0 的 InsertMany::exec_with_returning 给 Ok([])。
  • on_conflict_do_nothing() 的目标是主键,按唯一列要用 on_conflict_do_nothing_on。
  • 2.0.4 和 1.1.20 行为相同;issue #2389 关闭了,但 TryInsert::exec_with_returning 那条路在 2.0.4 上仍是 RecordNotFound。

💬 关联讨论

发起讨论
加载中...