$setOnInsert仅在upsert且匹配失败、MongoDB新建文档时生效,用于设置初始字段;而$set每次更新都执行。必须配合upsert: true使用,不能单独使用,也不适用于insertOne或replaceOne。

什么是 $setOnInsert,它和普通 $set 有什么区别?
$setOnInsert 只在文档**首次插入**(即 upsert 且匹配不到现有文档)时生效;如果只是更新已有文档,它完全不执行。这点和 $set 截然不同——$set 每次都写,不管文档是否存在。
常见错误是误以为 $setOnInsert 能“补全缺失字段”,比如对已存在文档再跑一次 upsert,结果发现字段没更新,就是因为它根本没触发。
- 必须配合
upsert: true使用,单独用$setOnInsert不起作用 - 只对
_id匹配失败、MongoDB 决定新建文档的那一刻生效 - 不能用于 replaceOne 或 insertOne,只能出现在 updateOne / updateMany 的 update 操作中
怎么写一个带 $setOnInsert 的 upsert 操作?
以 Node.js + MongoDB Driver 为例,关键在于 update 操作里同时包含 $set 和 $setOnInsert,并显式传入 { upsert: true }:
db.collection('users').updateOne(
{ email: 'alice@example.com' },
{
$set: { lastLogin: new Date() },
$setOnInsert: { createdAt: new Date(), status: 'active', version: 1 }
},
{ upsert: true }
)
这里 lastLogin 每次都更新,而 createdAt、status、version 仅在用户第一次注册(即 email 不存在)时写入。后续登录只改 lastLogin。
-
$setOnInsert的字段值可以是字面量、Date、ObjectId,甚至表达式(如$concat),但不能引用文档现有字段(因为此时文档还不存在) - 如果
$setOnInsert里写了已存在的字段,不会报错,但也不会覆盖——它压根不执行 - 注意:
$setOnInsert无法与$inc或$push等更新操作共用同一个字段名,否则行为未定义
为什么 upsert 后 createdAt 总是 null 或没写进去?
最常见原因是查询条件意外匹配到了旧文档,导致 MongoDB 执行的是“更新”而非“插入”。比如:
- 查询条件用了模糊匹配(如
{ name: /alice/i }),结果命中了已有记录 - 字段类型不一致:数据库里
email是字符串,但代码传了new ObjectId('...')当作查询条件 - 使用了
updateMany却期望单条插入逻辑——$setOnInsert在 updateMany 中仍只对每条新插入的文档生效,但容易因批量匹配引发误判 - Driver 版本太老($setOnInsert 支持不完整,建议确认 driver 版本 ≥ 4.0
调试时可先用 find 手动查一遍条件是否真没命中,再执行 upsert;或者临时加个 $unset: { debug: '' } 配合日志观察实际执行路径。
能否用 $setOnInsert 初始化嵌套字段或数组?
可以,语法一样,但要注意路径写法和初始化语义:
{
$setOnInsert: {
"profile.avatar": "default.png",
"roles": ["user"],
"settings.theme": "light"
}
}
这样会直接创建完整路径,不需要预先定义 schema。但要注意:
- 如果字段路径含点号(如
profile.avatar),MongoDB 会自动创建中间对象(profile) - 数组字段(如
roles)会被设为指定值,不是追加;已有文档即使有roles字段,这个操作也完全不触发 - 避免在
$setOnInsert中用$操作符(如$[elem]),它不支持数组过滤器上下文
真正容易被忽略的是:$setOnInsert 的字段一旦写入,后续就彻底“冻结”了——除非你手动 $set 去改,否则连管理员也无法通过 upsert 覆盖它。设计初始字段时得想清楚哪些真该“只设一次”。

















