MongoDB中upsert不是独立命令,而是updateOne、updateMany和findOneAndUpdate的可选参数;设upsert:true后,匹配则更新、不匹配则插入,配合$set和$setOnInsert可精准控制字段行为。

updateOne 里设 upsert: true 就行,别用 replaceOne
MongoDB 没有单独的 “upsert” 命令,它只是 updateOne、updateMany 和 findOneAndUpdate 的一个选项。用 replaceOne 是错的——它只会替换整条文档,且默认不 upsert,即使加 {upsert: true} 也容易因字段缺失导致意外覆盖。
正确姿势是明确用 updateOne,配合 $set 或 $setOnInsert 控制字段行为:
-
updateOne({ _id: 123 }, { $set: { name: "Alice", updatedAt: new Date() } }, { upsert: true }):存在就更新指定字段,不存在就插入完整文档(含_id和$set里的所有字段) - 想让某些字段“只在插入时生效”,比如
createdAt,得用$setOnInsert:{ $set: { updatedAt: new Date() }, $setOnInsert: { createdAt: new Date() } } - 如果过滤条件不含
_id,务必确认该字段有唯一索引,否则可能误插多条(比如用{ email: "a@b.com" }upsert 却没建email唯一索引)
upsert 后怎么拿到新插入文档的 _id?用 findOneAndUpdate
updateOne 返回结果里没有新文档内容,只有 matchedCount、modifiedCount、upsertedId(仅当 upsert 发生时才有)。如果你需要立刻读取刚插入或更新后的完整数据,必须用 findOneAndUpdate 并设 returnDocument: "after":
db.users.findOneAndUpdate(
{ email: "user@example.com" },
{
$set: { status: "active", updatedAt: new Date() },
$setOnInsert: { createdAt: new Date(), version: 1 }
},
{ upsert: true, returnDocument: "after" }
)
这样返回的就是最终的文档对象,不用再查一次。注意:Node.js 驱动里这个选项叫 returnDocument,不是 returnNewDocument(旧版已弃用)。
为什么有时 upsert 插了两条重复文档?
最常见原因是过滤条件不唯一,又没建对应唯一索引。例如:
- 执行
updateOne({ username: "bob" }, ..., { upsert: true }) - 但
username字段没建唯一索引 - 并发请求同时进来,都判断“没找到”,于是都执行插入 → 两条
username: "bob"文档
解决办法只有两个:要么加唯一索引(推荐):db.users.createIndex({ username: 1 }, { unique: true });要么改用 findAndModify 类原子操作并捕获 11000 重复键错误后重试(复杂且不必要)。MongoDB 的 upsert 本身是原子的,但前提是 MongoDB 能靠索引快速判定“是否存在”。没索引,就只能靠应用层协调。
和 MySQL 的 INSERT ... ON DUPLICATE KEY UPDATE 对比要注意啥?
语义接近,但行为差异明显:
- MySQL 依赖
UNIQUE或PRIMARY KEY约束触发“冲突”,MongoDB 依赖查询条件匹配 + 唯一索引保证不重复 - MySQL 可以在冲突时用
VALUES(col)引用原 INSERT 值;MongoDB 没等价语法,得靠$setOnInsert+ 应用层拼好完整更新对象 - MySQL 写入会锁住冲突的索引范围;MongoDB upsert 在有唯一索引时只锁匹配到的单个文档(或空范围),并发性能通常更好
- 驱动层面:PyMongo 的
update_one(..., upsert=True)和 Node.js 的collection.updateOne(..., { upsert: true })行为一致,但返回结构不同——Node.js 有upsertedId字段,PyMongo 返回的是UpdateResult.upserted_id,别漏判空值
真正容易被忽略的是:upsert 的“存在判断”完全基于你写的 filter 对象,而不是整个文档结构。哪怕你 filter 只写了 { a: 1 },它也会对所有 a === 1 的文档生效——不管其他字段是否一致。这点和业务上理解的“同一逻辑记录”可能不等价,得靠设计好主键或唯一索引兜底。


















