upsert不是独立命令,而是updateone等方法的布尔参数;设为true时先查filter再决定更新或插入,需配合$set和$setoninsert使用,并确保filter字段有唯一索引。

upsert 本质是 updateOne 的选项,不是独立命令
别搜“MongoDB upsert 命令”——它根本不存在。upsert只是 updateOne、updateMany 和 findOneAndUpdate 的一个布尔参数。设成 true 后,MongoDB 才会先查再决定走更新还是插入分支。
常见错误现象:用 replaceOne 加 { upsert: true },结果字段全丢、_id 冲突报错,甚至意外覆盖掉原文档里本不该动的字段。
-
replaceOne是整文档替换,不支持局部更新语义,也不该用来模拟 upsert - 想只改几个字段?必须用
$set或$setOnInsert显式包裹更新内容 - filter 条件里不含
_id,但 update 部分又写了_id字段?直接报错:Performing an insert operation on a collection with a field named _id
filter 必须稳定唯一,否则并发下会插出重复数据
upsert 判断“是否存在”的依据,就是拿你传的 filter 去查集合——只要查到至少一条,就更新;查不到,就插入。它不关心你 update 里改了啥,只认 filter 是否命中。
典型翻车场景:updateOne({ createdAt: { $gt: new Date() } }, { $set: { status: "pending" } }, { upsert: true }),每次时间都不同,永远查不到,结果每调一次就插一条。
- 推荐 filter 用业务唯一键,比如
{ userId: "u123" }、{ email: "a@b.com" } - 如果用了非
_id字段(如username),务必提前建唯一索引:db.users.createIndex({ username: 1 }, { unique: true }) - 没建索引?filter 字段无索引时,每次 upsert 都会全表扫描,写入延迟肉眼可见地涨
$set 和 $setOnInsert 必须配合使用,否则字段会丢
不加 $set 直接写字段名,比如 { name: "Alice", age: 30 },在 update 分支会删掉原文档其他所有字段(如 email、createdAt);在 upsert 插入分支倒是能用,但语义模糊、不可控。
正确姿势是始终用 $set 控制更新字段,再用 $setOnInsert 定义“仅新建时才有的字段”:
db.users.updateOne(
{ userId: "u123" },
{
$set: { name: "Alice", updatedAt: new Date() },
$setOnInsert: { createdAt: new Date(), version: 1 }
},
{ upsert: true }
)
-
$set在更新和插入时都生效;$setOnInsert只在真正插入新文档时才起作用 - 如果只想给缺失字段设默认值(比如
age字段不存在才设为 20),$setOnInsert不适用——它不检查字段是否存在,只看是否是本次插入 - 这种“按需补缺”需求,得用 MongoDB 5.0+ 的聚合管道更新,或应用层两步判断
事务中不能用 upsert: true,只能靠 $setOnInsert 模拟
在 session.startTransaction() 里调 updateOne(..., { upsert: true }),MongoDB 会直接拒绝,报错:Cannot specify 'upsert' option inside a transaction。这不是驱动问题,是服务端硬限制。
替代方案只有两个,且都得用 $setOnInsert:
- 显式两步:先
findOne({ session }),再根据结果updateOne或insertOne,所有操作挂同一个session - 更简洁安全的做法:直接
updateOne+$setOnInsert,不加upsert: true也行——因为$setOnInsert本身只在插入时触发,更新时自动忽略,天然符合事务语义 - 注意:
findOneAndUpdate在事务中也不能设upsert: true,但可以用returnDocument: "after"拿回最终文档,前提是操作本身成功
最容易被忽略的一点:upsert 成功后,updateOne 返回结果里没有新文档内容,只有 upsertedId(仅插入时有)、matchedCount 这类统计字段。真要立刻拿到完整数据,必须换用 findOneAndUpdate 并指定 returnDocument: "after"。别等写完再去查一次,多一次 round-trip,还可能读到中间态。











