MongoDB 从 4.2 版本开始支持把更新语句写成聚合管道,这让原本需要多次查询再回写的逻辑可以压缩到一次 updateMany 里完成。$cond 是管道更新中最常用的条件运算符,它负责在三元分支里根据布尔表达式返回不同结果。难点在于,真实集合里的字段经常处于 null、缺失、空字符串甚至类型不一致的状态,这些状态在条件判断中的行为并不完全相同。如果把判断条件写得过于简单,比如只比较字段是否等于某个字符串,就很容易把空值分支漏掉,最终写入错误的标签或默认值。本文会从嵌套 $cond 的写法入手,结合 $ifNull、$type 和 $switch,梳理出一套稳健的多分支更新方案。

先看一个典型需求:订单表中有 status 字段,值可能是 paid、refunded、pending,也可能是 null 或直接缺失。现在要根据不同状态批量写回一个中文状态标签,并且希望缺失或 null 都归入待处理。这个需求用普通更新表达式很难完成,因为普通 update 不支持一条语句里做 if else。聚合管道更新正好可以解决,但需要理解 $cond 的嵌套规则和空值边界。
$cond 表达式的基本语法与嵌套写法
$cond 在聚合管道中是一个三元条件运算符,等价于编程语言里的 if else。它有两种写法:一种是数组形式 { $cond: [布尔表达式, true 分支, false 分支] },另一种是对象形式 { $cond: { if: 布尔表达式, then: true 分支, else: false 分支 } }。两者功能一样,但对象形式在嵌套时更容易阅读,因为每个分支的含义都写得很清楚。更新管道里的 $set 阶段可以直接使用 $cond 计算字段值,而不需要先查出来再在应用层判断。
嵌套 $cond 的核心在于,把内层 $cond 放在外层 $cond 的 then 或 else 位置。比如先判断是否已支付,如果不是再判断是否已退款,最后才是待处理。这样每个条件分支都只走一条路径,不会出现多个分支同时命中的情况。下面这段代码用一条 updateMany 给所有订单补充状态标签。这里虽然只有三层判断,但已经出现了嵌套的可读性问题:如果继续增加分支,代码缩进会越来越深,逻辑链也会变得难以维护。
db.orders.updateMany(
{},
[
{
$set: {
statusLabel: {
$cond: {
if: { $eq: ["$status", "paid"] },
then: "已支付",
else: {
$cond: {
if: { $eq: ["$status", "refunded"] },
then: "已退款",
else: "待处理"
}
}
}
}
}
}
]
)
这个示例假设 status 字段一定存在且取值在预期范围内。但现实数据往往不是这样,比如字段缺失、值为 null、值为空字符串,或者混入了意外类型。一旦出现这些情况,$eq 判断就可能全部落空,最后统一返回 else 分支。这样虽然不会报错,但会把所有异常值都折叠成待处理,丢失了区分空值和真实 pending 的能力。
空值判断的边界:null、缺失与空字符串
MongoDB 查询过滤器里 { field: null } 会同时匹配字段值为 null 和字段缺失两种文档。但在聚合管道表达式中,直接引用缺失字段的行为并不完全等同于 null。为了保证更新结果稳定,最好的做法是不要假设缺失字段和 null 完全一致,而是用专门的运算符把状态显式归一化。$ifNull 就是这样一个工具,它接收一个字段表达式和一个默认值,如果字段为 null 或缺失,就返回默认值,否则返回字段本身。
下面这个例子根据 score 字段写回评分等级。score 可能缺失或为 null,此时返回未评分;如果 score 存在,就进入分数区间判断。这里用 { $ne: [{ $ifNull: ["$score", null] }, null] } 作为外层条件,把 null 和缺失统一拦截在 else 分支。需要注意的是,$ifNull 只能识别 null 和缺失,不能识别空字符串。如果 score 字段是空字符串,它仍然会进入 then 分支,后续的数值比较就可能出错。
db.users.updateMany(
{},
[
{
$set: {
scoreLevel: {
$cond: {
if: { $ne: [{ $ifNull: ["$score", null] }, null] },
then: {
$cond: {
if: { $gte: ["$score", 90] },
then: "优秀",
else: {
$cond: {
if: { $gte: ["$score", 60] },
then: "及格",
else: "不及格"
}
}
}
},
else: "未评分"
}
}
}
}
]
)
这段代码解决了 null 和缺失的问题,但如果 score 是字符串类型的数字或者空字符串,$gte 比较会抛错或者产生不符合预期的结果。因此,在涉及数值区间判断时,还需要进一步检查字段类型,而不只是判断是否为 null。空字符串是很容易被忽略的状态,尤其是在历史数据导入或表单未清理的情况下。一个稳健的更新管道应该把类型检查和空值检查结合起来。
用 $type 和 $switch 构建健壮的嵌套条件
当字段既可能是数字、字符串数字、空字符串,也可能是 null 时,仅靠 $ifNull 无法完全堵住类型错误。MongoDB 的 $type 运算符可以返回值的 BSON 类型字符串:对缺失字段返回 missing,对 null 返回 null,对空字符串返回 string,对整数返回 int,对浮点数返回 double。在 if 条件中先判断类型,可以避免后续数值比较误入非数字字段。更关键的是,当条件分支越来越多时,嵌套 $cond 会变得像多层缩进的 if else,阅读和调试成本都很高。MongoDB 提供了 $switch 运算符,作用类似编程语言里的 switch case,能够把多分支条件拍平,让每个 case 独立表达。
下面这段代码用 $switch 替代深层嵌套 $cond,同时处理缺失、null、空字符串和类型异常。$switch 的 branches 数组按顺序匹配,命中第一个 case 后立即返回对应 then,不再继续匹配后续分支。这里先拦截 missing 和 null,再拦截空字符串,然后排除非数字类型,最后才进行数值区间判断。顺序很重要,因为空字符串本身也属于 string 类型,如果先判断类型,空字符串会落入类型无效而不是未评分。
db.users.updateMany(
{},
[
{
$set: {
scoreLevel: {
$switch: {
branches: [
{
case: { $in: [{ $type: "$score" }, ["missing", "null"]] },
then: "未评分"
},
{
case: { $eq: ["$score", ""] },
then: "未评分"
},
{
case: { $not: [{ $in: [{ $type: "$score" }, ["int", "long", "double", "decimal"]] }] },
then: "分数类型无效"
},
{
case: { $gte: ["$score", 90] },
then: "优秀"
},
{
case: { $gte: ["$score", 60] },
then: "及格"
}
],
default: "不及格"
}
}
}
}
]
)
这个管道把多种异常状态分流得比较清楚。missing 和 null 返回未评分,空字符串也返回未评分,其他非数字类型返回分数类型无效,数字则进入成绩分级。相比前面层层嵌套的 $cond,$switch 的阅读体验明显更好,后续增加新分支也只需要在 branches 里继续追加一个对象,不会改变整体缩进结构。
实战案例:电商订单状态与会员权益批量更新
再看一个更贴近实际业务的多字段更新场景。假设订单集合里有 paymentStatus、fulfillmentStatus 和 memberLevel 三个字段,其中 paymentStatus 可能存在 null,fulfillmentStatus 可能缺失,memberLevel 可能是空字符串。现在要实现这样的规则:已支付且已发货的订单标记为已完成;已支付但未发货的标记为待发货;未支付的订单标记为待支付;只要 paymentStatus 为 null 或缺失,就标记为状态异常。这个需求如果放在应用层拼接,需要先查出来再逐条更新,数据量大时效率很低。使用聚合更新管道可以一次性完成。
下面这段代码用嵌套 $cond 处理 paymentStatus 的主分支,再在内部继续判断 fulfillmentStatus。为了不让 null 和缺失混入正常分支,外层先用 $ifNull 把 paymentStatus 归一化。memberLevel 的空字符串则单独用 $eq 判断。完整规则虽然不复杂,但条件层级已经有四层,实际业务里还可能继续增加。你可以把内层分支逐步替换成 $switch,或者将多个字段的判断拆成多个 $set 阶段,以提高可维护性。
db.orders.updateMany(
{},
[
{
$set: {
orderState: {
$cond: {
if: { $eq: [{ $ifNull: ["$paymentStatus", null] }, null] },
then: "状态异常",
else: {
$cond: {
if: { $eq: ["$paymentStatus", "paid"] },
then: {
$cond: {
if: { $eq: ["$fulfillmentStatus", "shipped"] },
then: "已完成",
else: "待发货"
}
},
else: "待支付"
}
}
}
},
memberLevel: {
$cond: {
if: { $eq: ["$memberLevel", ""] },
then: "普通会员",
else: "$memberLevel"
}
}
}
}
]
)
这段代码同时更新 orderState 和 memberLevel 两个字段,orderState 使用了三层嵌套 $cond,memberLevel 则处理了空字符串。需要注意的是,在同一个 $set 阶段中,两个字段的计算互不影响,不能在一个字段的表达式中读取另一个字段刚算出来的结果。如果后续字段依赖前一个字段的新值,就需要拆成多个 $set 阶段,管道会按顺序执行。对于空值处理来说,最关键的原则是把 null、缺失、空字符串和类型异常分开考虑,不要用同一种判断方式统一处理,否则总会有边界数据漏网。掌握了 $cond、$ifNull、$type 和 $switch 的组合,就能在一条更新语句里写出稳定、可读、易维护的多分支逻辑。

