MongoDB事务内不支持updateOne的upsert: true选项,必须用findOne+条件update/insert或$updateOnInsert替代;$setOnInsert是唯一官方允许的事务内upsert模拟方式,仅在插入时生效且需配合updateOne使用。

事务中直接用 updateOne(upsert: true) 会报错
MongoDB 事务内不支持 upsert: true 作为 updateOne() 的选项 —— 这是硬性限制,不是驱动或版本问题。你如果在 session.startTransaction() 后调用 collection.updateOne(query, update, { upsert: true }),会收到类似 "Cannot specify 'upsert' option inside a transaction" 的错误。
替代方案:先 find 再条件 update 或 insert
必须拆成两步,但需保证原子性:在事务中用 findOne() 检查是否存在,再根据结果决定调用 updateOne() 还是 insertOne()。关键点在于:
- 所有操作必须在同一
session下执行,且不能跨事务边界 -
findOne()必须带session参数,否则读取可能不在事务快照内 - 检查和写入之间无竞态,因为事务提供可串行化隔离(readConcern: "snapshot")
- 若业务逻辑允许,优先用
updateOne()+$setOnInsert组合,避免显式判断
示例片段(Node.js):
await session.withTransaction(async () => {
const doc = await collection.findOne(query, { session });
if (doc) {
await collection.updateOne(query, { $set: updateFields }, { session });
} else {
await collection.insertOne({ ...query, ...updateFields }, { session });
}
});$setOnInsert 是事务内最安全的 upsert 替代语法
这是唯一被 MongoDB 明确允许在事务中模拟 upsert 行为的方式。它只在插入时生效,更新时自动忽略,天然规避了并发写入冲突。
-
$setOnInsert必须配合updateOne()使用,不能单独存在 - 它只影响「本次操作触发插入」的场景;若文档已存在,该字段不会被修改
- 注意:它无法实现「存在则追加数组元素、不存在则新建对象」这类复杂逻辑,仅适用于字段级覆盖
- 常见误用:
$set和$setOnInsert混用时,$set仍会无条件更新已有字段,要小心覆盖语义
正确写法:
collection.updateOne(
{ _id: 123 },
{
$set: { lastModified: new Date() },
$setOnInsert: { createdAt: new Date(), status: "pending" }
},
{ session }
)嵌套数组 upsert 场景必须用聚合管道更新
如果你要更新的是「数组中某个对象的子数组」,比如向 items.0.sub_list 追加值,且要求 item 不存在时自动创建,那 $setOnInsert 不够用。此时只能依赖 MongoDB 4.2+ 的聚合管道更新能力,但它本身不支持事务内 upsert 标志 —— 所以你得把整个逻辑写进 pipeline,并确保 query 能精准命中或兜底。
- 必须用
updateOne(query, [pipeline], { session })形式,pipeline 内用$cond+$map+$concatArrays实现分支逻辑 - 所有数组访问(如
"$list.item")必须包裹$ifNull,否则空字段会导致 pipeline 中断 - 事务中 pipeline 更新仍受写关注(write concern)约束,失败会回滚整个事务
- 性能敏感场景慎用:每次都要遍历整个数组,大数据量下不如应用层分步处理清晰
真正容易被忽略的是:事务中 pipeline 更新虽能做复杂 upsert-like 操作,但它**不创建新文档** —— 如果 query 完全不匹配,update 就什么都不做。所以你仍需在外层判断是否 matchedCount === 0,再补一次 insertOne()。


















