mongodb 5.0+ 支持在 updateone/updatemany 中使用聚合管道实现基于当前值的计算更新;必须传入数组形式的 pipeline,字段引用写为 "$field",系统变量为 "$$now",嵌套字段需用字符串路径如 "profile.age",避免字面量误用和 $function 等高危操作。

updateOne 里用 pipeline 而不是 simple update
MongoDB 5.0+ 支持在 updateOne、updateMany 中传入聚合管道(而非单个更新文档),这是实现“基于当前值计算”的唯一可靠方式。老写法如 { $inc: { count: 1 } } 只能做简单原子操作;真要读取当前字段再加工(比如 price * 1.1 或字符串拼接),必须走 pipeline。
常见错误是直接把 $set 和 $expr 混在普通更新里,结果报错 Cannot use expression in update without pipeline —— 这说明你没传数组,而是传了对象。
- 正确调用形式:
collection.updateOne({ _id: id }, [ { $set: { total: { $multiply: ["$price", 1.1] } } } ]) - 第一个参数是 query,第二个必须是数组(哪怕只有一 stage)
- pipeline 内所有表达式都用
$开头,字段引用用"$field"(带引号的字符串)
$set + 表达式里的字段引用容易漏掉 $ 前缀
在 pipeline 的 $set 阶段里写 price: { $add: ["price", 10] } 是错的——"price" 被当字面量字符串,不是读取文档字段。必须写成 "$price",否则计算结果是 NaN 或空值,且不报错,极难排查。
典型场景:给用户余额加 5%,同时记录更新时间:{ $set: { balance: { $multiply: ["$balance", 1.05] }, updatedAt: { $toDate: "$$NOW" } } }
-
"$balance"→ 读当前文档的 balance 字段 -
"$$NOW"→ 使用系统变量(双 $),注意不是"$NOW" - 所有字段路径、变量、字面量必须严格区分:字符串字面量不带
$,字段引用带"$xxx",系统变量带"$$xxx"
updateMany 用 pipeline 时性能和原子性没变差
有人担心 pipeline 更新比普通 $inc 慢,其实只要 pipeline 不含 $lookup 或复杂子查询,性能几乎一致。MongoDB 在服务端对每个匹配文档独立执行 pipeline,仍是原子操作,不会出现读-改-写竞态。
但要注意:pipeline 更新不能用 upsert: true 自动创建文档,除非你在 pipeline 里显式处理缺失字段(比如用 $ifNull)。否则字段不存在时,"$xxx" 返回 null,后续计算可能崩。
- 安全写法:
{ $set: { score: { $add: [{ $ifNull: ["$score", 0] }, 1] } } } - 避免在 pipeline 里调用自定义 JavaScript(
$function),它禁用在分片集群,且慢 - 测试时用
findOne先验证 pipeline 行为,别直接updateMany
嵌套字段更新要展开路径,不能用点号简写
想更新 user.profile.age,不能写 "$user.profile.age" —— MongoDB 不支持点号路径在表达式里直接引用。必须先用 $set 提取,或用 $getField(5.0+)。
更实际的做法是用 $mergeObjects 或分步 $set:
[
{ $set: { "profile.age": { $add: [{ $ifNull: ["$profile.age", 0] }, 1] } } }
]
这个写法合法,因为 "profile.age" 是目标字段路径(字符串),而右边表达式仍用 "$profile.age" 读值。关键点在于:左边是字段名字符串,右边是字段引用字符串,二者语法位置不同,不能互换。
容易被忽略的是数组元素更新,比如给 tags 数组追加一个值:用 { $push: { tags: "new" } } 更直接,pipeline 里硬写 $concatArrays 反而绕路。










