$redact 是基于表达式对文档每个节点做“保留/屏蔽/递归”三选一决策的结构遍历工具;它不投影字段也不筛选数组元素,而是深度控制嵌套结构中各节点的可见性。

什么是 $redact,它和 $project/$filter 有什么本质区别
$redact 不是字段投影工具,也不是条件筛选数组元素的函数。它的核心作用是在**遍历整个文档结构(包括嵌套对象和数组)时,基于表达式对每个节点做“保留/屏蔽/递归”三选一决策**。这意味着:你不能用它“只留 name 和 email”,但可以用它“当用户无权限时,把整个 salary 字段替换成 $$PRUNE,且不碰其他字段”。常见错误是把它当 $project 用,结果发现字段没删干净,或者深层嵌套字段意外被删了。
权限判断逻辑必须写在 $cond 表达式里,且注意作用域
$redact 的输入必须是 $cond 或其它能返回 $$KEEP / $$PRUNE / $$DESCEND 的表达式。关键点在于:表达式里访问不到外部变量(比如用户角色),必须提前把权限信息注入文档——通常靠 $mergeObjects 或 $let 配合 $map 实现。例如,假设当前用户角色是 "editor",且权限规则存在 permissions 数组中:
{
$redact: {
$cond: {
if: { $and: [
{ $eq: ["$field", "salary"] },
{ $not: { $in: ["editor", "$$ROOT.permissions"] } }
]},
then: "$$PRUNE",
else: "$$DESCEND"
}
}
}
这里 $$ROOT.permissions 能取到,前提是上一步已经把权限数组挂到了根层级;如果权限在变量里,得用 $let 绑定再引用。
$$PRUNE 和 $$DESCEND 容易混淆,尤其在数组场景下
对数组元素调用 $$PRUNE,会删掉**整个数组项**,不是删掉该项里的某个字段;而 $$DESCEND 才会让聚合继续深入该数组项内部逐字段判断。典型陷阱:
- 想隐藏用户数组中所有
email字段?得让$redact进入每个用户对象,再判断其email字段——这时必须用$$DESCEND进入数组,否则$$PRUNE会直接干掉整个用户对象 - 误把
$$KEEP当默认行为:没匹配到任何条件时,$redact默认行为是$$KEEP,但一旦写了$cond却漏了else分支,就会报错 - 嵌套对象里有同名字段(如
user.profile.phone和user.contact.phone),单靠"$field" === "phone"无法区分路径,得结合$$CURRENT和$type做上下文判断
实际聚合管道中,$redact 必须放在 $project 之前且紧邻权限注入步骤
顺序错了就白忙:$redact 必须在文档已含权限字段后立即执行,且不能被后续 $project 提前截断结构(因为 $project 会丢掉未显式列出的字段,导致 $redact 后续没法处理)。推荐结构:
[
{ $addFields: { permissions: ["editor"] } },
{ $redact: { ... } },
{ $project: { _id: 1, name: 1, email: 1 } }
]
注意:如果 $project 放在 $redact 前,$redact 就只能看到被投射后的字段,深层结构早没了;如果 $redact 后还接了 $unwind,要小心被 $$PRUNE 删掉的数组项会导致 $unwind 报错(空数组),得加 preserveNullAndEmptyArrays: true。
真正难的不是语法,而是理清“权限判定发生在哪一层、对哪个路径生效、删的是值还是容器”。一次写不对很正常,建议先用 $replaceRoot + $literal 模拟权限字段,再逐步放开真实字段验证路径逻辑。











