Rust in Everything
基于 sea-orm 2.0.4

insert

主线位置:写入主线的 insert 分支。am.insert(&db) 是 ActiveModelTrait::insert:before_save(self, db, true) → Entity::insert(am) 造一个 Insert<A>(里面是 sea-query 的 InsertStatement)→ exec_with_returning(db) → after_save。这一页讲中间两步:Insert 怎么从 ActiveModel 挑列,几个 exec 各加什么 RETURNING、返回什么;insert_many 怎么对齐各行的列;on_conflict 和冲突时的结果。钩子在 save 与 ActiveModelBehavior。

flowchart TB
  am[ActiveModel] -->|"Insert::one"| ins["Insert A"]
  ams["多个 ActiveModel"] -->|"InsertMany::many"| many["InsertMany A"]
  many -->|空列表| empty["不发 SQL"]
  many --> ins
  ins -->|exec| pk["RETURNING 主键"]
  ins -->|exec_with_returning| all["RETURNING 全部列"]
  ins -->|exec_without_returning| cnt["rows_affected"]
  ins -->|try_insert 等| tri["TryInsert A"]
  tri --> res["Inserted / Conflicted / Empty"]

实验沿用上一页的 item 表(id serial primary key、sku text not null unique、name text not null、qty int not null default 0、note text),每个程序开头重建表并插入三行 a1、b2、c3。SQL 取自 sqlx 的语句日志(sqlx=info,带 $N、不带值),要看值时另外 build(DbBackend::Postgres) 打印。平台 M2 Max、macOS、rustc 1.99.0、Postgres 16.14,sea-orm 2.0.4;对比用的 1.1.20 在单独的 crate 里。

Insert::one:有值的列都写

Insert::one 逐列 take,Set 和 Unchanged 都进列表:

    pub fn one<M>(m: M) -> Self
    where
        M: IntoActiveModel<A>,
    {
        let mut query = InsertStatement::new();
        query
            .into_table(A::Entity::default().table_ref())
            .or_default_values();

        let mut am: A = m.into_active_model();
        let primary_key =
            if !<<A::Entity as EntityTrait>::PrimaryKey as PrimaryKeyTrait>::auto_increment() {
                am.get_primary_key_value()
            } else {
                None
            };
        let mut columns = Vec::new();
        let mut values = Vec::new();

        for col in <A::Entity as EntityTrait>::Column::iter() {
            let av = am.take(col);

            match av {
                ActiveValue::Set(value) | ActiveValue::Unchanged(value) => {
                    columns.push(col);
                    values.push(col.save_as(Expr::val(value)));
                }
                ActiveValue::NotSet => {}
            }
        }

        query.columns(columns);
        query.values_panic(values);
        // ...
    }

三处值得看。NotSet 的列不出现在 SQL 里,Postgres 用列的默认值(serial 的 nextval、default 0)。primary_key 只在主键不是自增时记下,给不支持 RETURNING 的 MySQL 用来充当 last_insert_id;Postgres 上它不起作用。or_default_values() 是给「一列都没有」准备的:sea-query 的 insert_default_keyword 在 Postgres 上写 VALUES (DEFAULT),下面的实验里会看到。

参数 M: IntoActiveModel<A>,所以 Insert::one 也收 Model:一个 Model 转成全 Unchanged,所有列(包括主键)都会写进去。

四个 exec,三种 RETURNING

Insert<A> 的执行方法在 executor/insert.rs,区别在加不加 RETURNING、加哪几列:

方法RETURNING返回
exec主键列InsertResult { last_insert_id }
exec_with_returning全部列Model
exec_without_returning无u64(rows_affected)
exec_with_returning_keys / exec_with_returning_many主键 / 全部列Vec

exec 在 exec_insert 里按后端分三路:

    let last_insert_id = match (primary_key, db.support_returning()) {
        (_, true) => {
            let mut rows = db.query_all(&statement).await?;
            let row = match rows.pop() {
                Some(row) => row,
                None => return Err(DbErr::RecordNotInserted),
            };
            let cols = PrimaryKey::<A>::iter()
                .map(|col| col.to_string())
                .collect::<Vec<_>>();
            row.try_get_many("", cols.as_ref())
                .map_err(|_| DbErr::UnpackInsertId)?
        }
        (Some(value_tuple), false) => {
            let res = db.execute(&statement).await?;
            if res.rows_affected() == 0 {
                return Err(DbErr::RecordNotInserted);
            }
            FromValueTuple::from_value_tuple(value_tuple)
        }
        (None, false) => {
            let res = db.execute(&statement).await?;
            // ...
            let last_insert_id = res.last_insert_id();
            // ...
        }
    };

Postgres 走第一路:last_insert_id 就是 RETURNING 回来的最后一行的主键;没有返回行就是 RecordNotInserted(None of the records are inserted),这是后面 DO NOTHING 的伏笔。exec_insert_with_returning 则把每一列的 select_as(into_returning_expr(..)) 放进 RETURNING,用 ReturningSelector::one 读回一个 Model;不支持 RETURNING 的后端先 exec_insert 再按 last_insert_id find_by_id,那是两条语句。

实验:每个 exec 发出什么

src/bin/insert_one.rs,pear() 是一个只设了 sku 和 name 的 ActiveModel:

let pear = || item::ActiveModel { sku: Set("p1".into()), name: Set("pear".into()), ..Default::default() };

let stmt = item::Entity::insert(pear()).build(DbBackend::Postgres);
println!("build: {} {:?}", stmt.sql, stmt.values.map(|v| v.0).unwrap_or_default());

println!("--- ActiveModel::insert");
let m = pear().insert(&db).await?;
println!("=> {m:?}");

println!("--- Entity::insert(..).exec");
let mut am = pear();
am.sku = Set("p2".into());
let r = item::Entity::insert(am).exec(&db).await?;
println!("=> last_insert_id {}", r.last_insert_id);

println!("--- exec_without_returning");
// ... sku 换成 p3,调 exec_without_returning

println!("--- nothing Set");
let r = item::Entity::insert(<item::ActiveModel as Default>::default()).exec(&db).await;
println!("=> {r:?}");

println!("--- insert a Model read back (all Unchanged)");
let a1 = item::Entity::find_by_id(1).one(&db).await?.unwrap();
let r = a1.into_active_model().insert(&db).await;
// ... 打印 Ok 或 Err 的 Display
build: INSERT INTO "item" ("sku", "name") VALUES ($1, $2) [String(Some("p1")), String(Some("pear"))]
--- ActiveModel::insert
 INFO sqlx::query: summary="INSERT INTO \"item\" (\"sku\", …" db.statement="\n\nINSERT INTO \"item\" (\"sku\", \"name\") VALUES ($1, $2) RETURNING \"id\", \"sku\", \"name\", \"qty\", \"note\"\n" rows_affected=1 rows_returned=1 elapsed=2.623625ms elapsed_secs=0.002623625
=> Model { id: 4, sku: "p1", name: "pear", qty: 0, note: None }
--- Entity::insert(..).exec
 INFO sqlx::query: summary="INSERT INTO \"item\" (\"sku\", …" db.statement="\n\nINSERT INTO \"item\" (\"sku\", \"name\") VALUES ($1, $2) RETURNING \"id\"\n" rows_affected=1 rows_returned=1 elapsed=927.667µs elapsed_secs=0.000927667
=> last_insert_id 5
--- exec_without_returning
 INFO sqlx::query: summary="INSERT INTO \"item\" (\"sku\", …" db.statement="\n\nINSERT INTO \"item\" (\"sku\", \"name\") VALUES ($1, $2)\n" rows_affected=1 rows_returned=0 elapsed=819.166µs elapsed_secs=0.000819166
=> 1
--- nothing Set
 INFO sqlx::query: summary="INSERT INTO \"item\" VALUES …" db.statement="\n\nINSERT INTO \"item\" VALUES (DEFAULT) RETURNING \"id\"\n" rows_affected=0 rows_returned=0 elapsed=611.417µs elapsed_secs=0.000611417
=> Err(Query(SqlxError(Database(PgDatabaseError { severity: Error, code: "23502", message: "null value in column \"sku\" of relation \"item\" violates not-null constraint", detail: Some("Failing row contains (7, null, null, 0, null)."), hint: None, position: None, where: None, schema: Some("so_write1"), table: Some("item"), column: Some("sku"), data_type: None, constraint: None, file: Some("execMain.c"), line: Some(2057), routine: Some("ExecConstraints") }))))
--- insert a Model read back (all Unchanged)
 INFO sqlx::query: 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=906.125µs elapsed_secs=0.000906125
 INFO sqlx::query: summary="INSERT INTO \"item\" (\"id\", …" db.statement="\n\nINSERT INTO \"item\" (\"id\", \"sku\", \"name\", \"qty\", \"note\") VALUES ($1, $2, $3, $4, $5) RETURNING \"id\", \"sku\", \"name\", \"qty\", \"note\"\n" rows_affected=0 rows_returned=0 elapsed=765.084µs elapsed_secs=0.000765084
=> Err Query Error: error returned from database: duplicate key value violates unique constraint "item_pkey" at line 666

每次写入都是一条语句。build 的结果里没有 RETURNING,它是执行方法加上去的;ActiveModel::insert 用的是 exec_with_returning,所以 qty 的默认值 0 和数据库分配的 id 都随这一条语句回来。一列都没 Set 时发出的是 VALUES (DEFAULT),这张表的 sku 没有默认值,报 23502。最后一段是常见的误用:读出来的 Model 全是 Unchanged,insert 把 id 也写进去,撞上主键(错误末尾的 at line 666 是 sqlx 0.9 的 Display 加的服务端源码行号,见 sqlx 卷的错误)。想「复制一行」得先把主键置成 NotSet。

insert_many:列取并集,缺的补 NULL

InsertMany::many 要把每行可能不同的列集合拼进一条 INSERT ... VALUES (..), (..),而 SQL 要求每行的列一样:

        for model in models.into_iter() {
            let mut am: A = model.into_active_model();
            // ...
            let mut values = Vec::with_capacity(columns.len());
            for (idx, col) in <A::Entity as EntityTrait>::Column::iter().enumerate() {
                let av = am.take(col);
                match av {
                    ActiveValue::Set(value) | ActiveValue::Unchanged(value) => {
                        columns[idx] = Some(col); // mark the column as used
                        null_value[idx] = Some(value.as_null()); // store the null value with the correct type
                        values.push(col.save_as(Expr::val(value))); // same as add() above
                    }
                    ActiveValue::NotSet => {
                        values.push(SimpleExpr::Keyword(Keyword::Null)); // indicate a missing value
                    }
                }
            }
            all_values.push(values);
        }

        let empty = all_values.is_empty();
        // ...
        for values in all_values {
            // since we've aligned the column set, this never panics
            query.values_panic(values.into_iter().enumerate().filter_map(|(i, v)| {
                if columns[i].is_some() {
                    // only if the column is used
                    if !matches!(v, SimpleExpr::Keyword(Keyword::Null)) {
                        // use the value expression
                        Some(v)
                    } else {
                        // use null as standin, which must be Some
                        null_value[i].clone().map(SimpleExpr::Value)
                    }
                } else {
                    None
                }
            }));
        }

任何一行设了的列都进列表;某一行没设的列,填进去的是 value.as_null(),一个带类型的 NULL 参数(Int(None) 这样的),不是 SQL 的 DEFAULT。所以只要有一行设了某列,其他行这一列就从「交给默认值」变成了「写 NULL」。src/bin/insert_many.rs 让两行里只有第二行设 qty:

let stmt = item::Entity::insert_many([am("m5", None), am("m6", Some(9))]).build(DbBackend::Postgres);
println!("values {:?}", stmt.values.map(|v| v.0).unwrap_or_default());
let r = item::Entity::insert_many([am("m5", None), am("m6", Some(9))]).exec(&db).await;
--- mixed: only the second sets qty
values [String(Some("m5")), String(Some("m5")), Int(None), String(Some("m6")), String(Some("m6")), Int(Some(9))]
 INFO sqlx::query: summary="INSERT INTO \"item\" (\"sku\", …" db.statement="\n\nINSERT INTO \"item\" (\"sku\", \"name\", \"qty\") VALUES ($1, $2, $3), ($4, $5, $6) RETURNING \"id\"\n" rows_affected=0 rows_returned=0 elapsed=644.5µs elapsed_secs=0.0006445
=> Err Query Error: error returned from database: null value in column "qty" of relation "item" violates not-null constraint at line 2057

第一行的 qty 绑的是 Int(None),qty 是 not null default 0,整条语句失败。单独插第一行(Insert::one)不会出这个错,因为 qty 根本不在列表里。每行设同样的列,或者在 Rust 里补上默认值,才能避开。

列一样时,exec 和 exec_with_returning 的结果:

--- same columns, exec
 INFO sqlx::query: summary="INSERT INTO \"item\" (\"sku\", …" db.statement="\n\nINSERT INTO \"item\" (\"sku\", \"name\") VALUES ($1, $2), ($3, $4) RETURNING \"id\"\n" rows_affected=2 rows_returned=2 elapsed=1.949666ms elapsed_secs=0.001949666
=> last_insert_id Some(5)
--- same columns, exec_with_returning
 INFO sqlx::query: summary="INSERT INTO \"item\" (\"sku\", …" db.statement="\n\nINSERT INTO \"item\" (\"sku\", \"name\") VALUES ($1, $2), ($3, $4) RETURNING \"id\", \"sku\", \"name\", \"qty\", \"note\"\n" rows_affected=2 rows_returned=2 elapsed=1.217791ms elapsed_secs=0.001217791
=> [Model { id: 6, sku: "m3", name: "m3", qty: 0, note: None }, Model { id: 7, sku: "m4", name: "m4", qty: 0, note: None }]

InsertMany::exec 返回 InsertManyResult { last_insert_id: Option<..> },Some(5) 是最后一行的主键(exec_insert 里的 rows.pop()),要全部主键用 exec_with_returning_keys。InsertMany::exec_with_returning 返回 Vec<Model>,它在内部调的是 exec_with_returning_many。

空列表

InsertMany 比 Insert 多一个 empty 字段,每个 exec 开头先看它:

    pub async fn exec<C>(self, db: &C) -> Result<InsertManyResult<A>, DbErr>
    where
        C: ConnectionTrait,
    {
        if self.empty {
            return Ok(InsertManyResult {
                last_insert_id: None,
            });
        }
        let res = self.into_one().exec(db).await;
        // ...
    }

exec_with_returning 返回 vec![],exec_without_returning 返回 0,都不碰数据库:

--- empty
exec => last_insert_id None
exec_with_returning => []
exec_without_returning => 0
--- end

--- empty 和 --- end 之间没有一行 sqlx 日志。

📝NOTE
冷知识:1.x 的 insert_many([]) 会插入一行默认值

1.1.20 里 insert_many 和 insert 共用一个 Insert 结构,Insert::many 从 Insert::new() 开始,而 new() 里有 or_default_values()。列表为空时一列也没有,生成的就是 INSERT INTO "item" VALUES (DEFAULT) RETURNING "id"(exp-seaorm-write1-old 的 write_old,1.1.20):

--- insert_many([]).exec
 INFO sqlx::query: summary="INSERT INTO \"item\" VALUES …" db.statement="\n\nINSERT INTO \"item\" VALUES (DEFAULT) RETURNING \"id\"\n" rows_affected=0 rows_returned=0 elapsed=1.019667ms elapsed_secs=0.001019667
=> Err(Query(SqlxError(Database(PgDatabaseError { severity: Error, code: "23502", message: "null value in column \"sku\" of relation \"item\" violates not-null constraint", detail: Some("Failing row contains (4, null, null, 0, null)."), hint: None, position: None, where: None, schema: Some("so_write1"), table: Some("item"), column: Some("sku"), data_type: None, constraint: None, file: Some("execMain.c"), line: Some(2057), routine: Some("ExecConstraints") }))))
...
--- insert_many([]).on_empty_do_nothing().exec
=> Ok(Empty)

item 有不带默认值的 not null 列,所以只是报错。换一张每列都有默认值的表(counter (id serial primary key, n int not null default 0),empty_old),同样的调用在 1.1.20 下真的插进一行:

insert_many([]).exec => Ok(InsertResult { last_insert_id: 1 })
rows now: [Model { id: 1, n: 0 }]

2.0.4 下(empty_new)什么都不发,表还是空的:

insert_many([]).exec => Ok(InsertManyResult { last_insert_id: None })
rows now: []

这个问题最早是 issue #873:「Sometimes, as a result of the mapping/filtering, the resulting iterator may be empty. In this case, the insert_many operation fails; I'd expect this to succeed and the database not being hit at all as there is nothing to do.」,日志里正是 VALUES (DEFAULT) RETURNING "id"。PR #1708(0.12.0-rc.3)加了 TryInsert 和要手动调用的 on_empty_do_nothing(),不调用时行为不变。2.0 的 PR #2628(「Insert many」)把 insert_many 拆成单独的 InsertMany,CHANGELOG 写着「removed APIs that can panic」「on empty iterator, None or vec![] is returned on exec operations」,last_insert_id 也因此变成 Option。仓库里最早包含这个提交的 tag 是 2.0.0-rc.28(更早的候选版本没有 tag)。本站还在 1.1.20 上:一个可能为空的列表交给 insert_many 之前要自己判断,或者加 on_empty_do_nothing()。

on_conflict:upsert 与 DO NOTHING

Insert::on_conflict(OnConflict) 把 sea-query 的 OnConflict 原样放进语句,OnConflict::column(..) 指定冲突目标,update_column(s) 生成 DO UPDATE SET col = "excluded".col,do_nothing() 生成 DO NOTHING。upsert 配 exec_with_returning,冲突时拿回的是更新后的那一行(src/bin/on_conflict.rs):

let upsert = OnConflict::column(item::Column::Sku).update_column(item::Column::Name).to_owned();
let m = item::Entity::insert(am("a1", "apple v2")).on_conflict(upsert.clone()).exec_with_returning(&db).await?;
--- upsert, exec_with_returning
 INFO sqlx::query: summary="INSERT INTO \"item\" (\"sku\", …" db.statement="\n\nINSERT INTO \"item\" (\"sku\", \"name\") VALUES ($1, $2) ON CONFLICT (\"sku\") DO UPDATE SET \"name\" = \"excluded\".\"name\" RETURNING \"id\", \"sku\", \"name\", \"qty\", \"note\"\n" rows_affected=1 rows_returned=1 elapsed=2.035542ms elapsed_secs=0.002035542
=> Model { id: 1, sku: "a1", name: "apple v2", qty: 3, note: None }

DO NOTHING 的麻烦在于冲突时 Postgres 不返回行,RETURNING 也就是空的。Insert::exec 把「没有返回行」当成 RecordNotInserted,于是一次「按设计什么也不做」的插入变成了一个错误。为此有 TryInsert:它包着同一个 Insert,执行方法把这个错误翻译成 TryInsertResult::Conflicted:

    pub async fn exec<C>(self, db: &C) -> Result<TryInsertResult<InsertResult<A>>, DbErr>
    where
        C: ConnectionTrait,
    {
        if self.empty {
            return Ok(TryInsertResult::Empty);
        }
        let res = self.insert_struct.exec(db).await;
        match res {
            Ok(res) => Ok(TryInsertResult::Inserted(res)),
            Err(DbErr::RecordNotInserted) => Ok(TryInsertResult::Conflicted),
            Err(err) => Err(err),
        }
    }

拿到 TryInsert 的办法有几个:Insert::try_insert()、on_conflict_do_nothing_on(cols)(冲突目标是 cols)、on_conflict_do_nothing()(冲突目标是主键),InsertMany 上也有同名的方法。2.0 把 Insert::do_nothing()、on_empty_do_nothing() 标成了 deprecated。对已有的 a1 再插一次,各种组合的结果:

--- do_nothing via on_conflict(..), exec
 INFO sqlx::query: 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=563.958µs elapsed_secs=0.000563958
=> Err(RecordNotInserted)
   Display: None of the records are inserted
--- on_conflict_do_nothing_on([Sku]), exec
 INFO sqlx::query: 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=181.5µs elapsed_secs=0.0001815
=> Ok(Conflicted)
--- same, exec_without_returning
 INFO sqlx::query: 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=330.541µs elapsed_secs=0.000330541
=> Ok(Inserted(0))
--- insert_many, one conflicts, on_conflict_do_nothing_on, exec
 INFO sqlx::query: summary="INSERT INTO \"item\" (\"sku\", …" db.statement="\n\nINSERT INTO \"item\" (\"sku\", \"name\") VALUES ($1, $2), ($3, $4) ON CONFLICT (\"sku\") DO NOTHING RETURNING \"id\"\n" rows_affected=1 rows_returned=1 elapsed=1.134375ms elapsed_secs=0.001134375
=> Ok(Inserted(InsertResult { last_insert_id: 9 }))
--- insert_many, all conflict, exec
 INFO sqlx::query: summary="INSERT INTO \"item\" (\"sku\", …" db.statement="\n\nINSERT INTO \"item\" (\"sku\", \"name\") VALUES ($1, $2), ($3, $4) ON CONFLICT (\"sku\") DO NOTHING RETURNING \"id\"\n" rows_affected=0 rows_returned=0 elapsed=432.292µs elapsed_secs=0.000432292
=> Ok(Conflicted)
--- insert_many, all conflict, exec_with_returning_many
 INFO sqlx::query: summary="INSERT INTO \"item\" (\"sku\", …" db.statement="\n\nINSERT INTO \"item\" (\"sku\", \"name\") VALUES ($1, $2), ($3, $4) ON CONFLICT (\"sku\") DO NOTHING RETURNING \"id\", \"sku\", \"name\", \"qty\", \"note\"\n" rows_affected=0 rows_returned=0 elapsed=743.75µs elapsed_secs=0.00074375
=> Ok(Inserted([]))
--- on_conflict_do_nothing() (primary key), sku clash
 INFO sqlx::query: 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=737.375µs elapsed_secs=0.000737375
=> Err Query Error: error returned from database: duplicate key value violates unique constraint "item_sku_key" at line 666

SQL 完全一样的两次,差别只在调用的是 Insert::exec 还是 TryInsert::exec。Conflicted 只有在「一行都没插进去」时出现:两行里一行冲突,结果是 Inserted,last_insert_id 是插进去的那一行。exec_without_returning 不看返回行,冲突时是 Inserted(0);exec_with_returning_many 冲突时是 Inserted([]),都要自己看数量。on_conflict_do_nothing() 的冲突目标是主键,sku 撞了照样报 23505:这张表的主键是 serial,插入时不带主键,它几乎永远用不上,要写 on_conflict_do_nothing_on([Column::Sku])。冲突目标后面跟着的 do_nothing_on(主键) 只对 MySQL 有意义(生成 ON DUPLICATE KEY UPDATE id = id),Postgres 上就是 DO NOTHING。

这些行为在 1.1.20 里也一样(write_old):on_conflict(do_nothing).exec 是 Err(RecordNotInserted),加 .do_nothing() 变成 TryInsert 后是 Ok(Conflicted)。2.0 改的是拿到 TryInsert 的方法名,冲突时的结果没变。

❗IMPORTANT
动脑筋:TryInsert 的 exec_with_returning 遇到冲突,返回哪一个?
let r = item::Entity::insert(am("a1", "x"))
  .on_conflict_do_nothing_on([item::Column::Sku])
  .exec_with_returning(&db)
  .await;
println!("=> {r:?}");

a1 已经存在。选项:Ok(Conflicted)、Ok(Inserted(..))、Err(..)。平台 M2 Max、macOS、rustc 1.99.0、Postgres 16.14。

动脑筋答案

--- same, exec_with_returning
 INFO sqlx::query: 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=381.042µs elapsed_secs=0.000381042
=> Err(RecordNotFound("Failed to find inserted item"))

是 Err,而且不是 RecordNotInserted。TryInsert::exec_with_returning 只把 RecordNotInserted 翻译成 Conflicted,可它调用的 exec_insert_with_returning 在没有返回行时报的是另一个错误:

    match found {
        Some(model) => Ok(model),
        None => Err(DbErr::RecordNotFound(
            "Failed to find inserted item".to_owned(),
        )),
    }

于是 Conflicted 这个分支在这个方法上永远走不到,冲突以 RecordNotFound 的形式冒出来。1.1.20 的 do_nothing().exec_with_returning 也是这个结果。要「插入并拿回整行,冲突时什么也不做」,可以用 exec_with_returning_many(冲突时 Inserted([])),或者用 exec 拿 Conflicted 再查一次。

小结

  • Insert::one 写 Set 和 Unchanged 的列,NotSet 交给默认值;一列都没有时是 VALUES (DEFAULT);读来的 Model 直接插入会带上主键。
  • exec 只 RETURNING 主键,last_insert_id 来自返回的最后一行;exec_with_returning 一条语句拿回整行;exec_without_returning 只给 rows_affected。
  • insert_many 取各行列的并集,某行没设的列补成带类型的 NULL,不是 DEFAULT,not null default 的列会因此失败。
  • 2.0 的 InsertMany 对空列表不发 SQL;1.1.20 发 VALUES (DEFAULT),可能报错,也可能插进一行默认值。
  • DO NOTHING 冲突时:Insert::exec 报 RecordNotInserted,TryInsert::exec 给 Conflicted,exec_without_returning 给 Inserted(0),exec_with_returning 报 RecordNotFound("Failed to find inserted item");1.1.20 相同。
  • on_conflict_do_nothing() 只针对主键,按唯一列去重要用 on_conflict_do_nothing_on。

💬 关联讨论

发起讨论
加载中...