在MongoDB聚合管道更新操作中能否实现$setOnInsert的等效功能?
背景
MongoDB近期新增了一项功能,支持通过传入聚合管道而非标准修改器对象执行update操作。聚合管道的语句可以引用文档已有属性,因此在需要基于其他字段计算特定字段的场景(比如数据迁移)中非常实用。
绝大多数标准更新运算符比如$set、$push、$inc等都可以用聚合表达式语言成功复现,从某种意义上而言这项新功能是传统修改器技术的泛化实现。不过如果要实现$addToSet这类操作,管道会变得相当冗长,也会带来一定性能损耗。
待解决的问题
目前无法通过聚合管道更新完全复现$setOnInsert运算符的功能。假设执行一次upsert操作:
db.test.update(selector, pipeline, { upsert: true });
常规思路中,默认会认为没有匹配selector的文档时,管道中的$$ROOT变量值为null,但实际MongoDB会将$$ROOT设置为从selector推导而来,这就导致无法在pipeline内部区分当前操作是更新还是插入。
判断$$ROOT._id的方案在_id是selector一部分的场景下会失效,目前可用的绕路写法如下:
selector = { _id: { $in: [value, 'fake'] }, }
用该写法替换更简单的{ _id: value }即可生效,但写法不够简洁规范。需要找到更优的解决方案,最好是存在某个隐藏变量可以直接在pipeline内部区分update和insert操作,类似$merge阶段中的$$new变量。
解决方案
截至MongoDB 7.0版本,官方没有提供聚合管道update场景下直接区分更新/插入操作的系统变量,可通过以下两种更规范的方案实现$setOnInsert的等效逻辑:
方案1:使用$ifNull判断业务字段
不要选择_id这类会出现在selector里的字段作为判断依据,选择仅在文档存在时才会出现的业务字段作为判断标记,不需要修改selector,写法简洁,适合绝大多数业务场景:
const pipeline = [ { $set: { // 仅插入时生效的字段,等效$setOnInsert createTime: { $ifNull: ["$createTime", new Date()] }, creator: { $ifNull: ["$creator", "system"] }, // 无论更新还是插入都生效的字段 updateTime: new Date(), updater: "user_001" } } ]
方案2:新增隐藏标记字段
如果业务上没有合适的通用判断字段,可以在所有文档中新增一个固定值的隐藏标记字段,插入后永远不会修改,兼容性更强,不受selector内容的影响:
const pipeline = [ { $set: { __existsFlag: { $ifNull: ["$__existsFlag", true] }, // 根据标记字段判断是插入还是更新 insertOnlyField: { $cond: { if: "$__existsFlag", then: "$insertOnlyField", else: "insert_default" } }, updateField: "new_value" } } ]
内容的提问来源于stack exchange,提问作者Tomasz Lenarcik

