
MarkLogic 的 post-commit 触发器默认启用递归(recursive=true),当触发器逻辑中通过 xdmp.documentInsert() 修改同一文档时,会再次触发自身,导致“一次更新、两次执行”的现象。
marklogic 的 post-commit 触发器默认启用递归(recursive=true),当触发器逻辑中通过 `xdmp.documentinsert()` 修改同一文档时,会再次触发自身,导致“一次更新、两次执行”的现象。
在 MarkLogic 应用开发中,触发器链式调用是常见设计模式,但若未显式控制递归行为,极易引发意外的重复执行——正如您所遇到的 validateData.sjs 触发器被调用两次的问题。根本原因不在于触发器注册重复或事务提交异常,而在于 MarkLogic 触发器的默认递归机制。
根据官方文档,trgr.createTrigger() 函数的 recursive 参数默认值为 true。这意味着:只要触发器内部操作(如 xdmp.documentInsert(uri, ..., 'export-ready'))导致目标文档发生内容变更(即使仅修改元数据如集合、权限或属性),且该变更匹配当前触发器的事件条件(例如 "collection-scope": {"uri": "processed"} 且 "update-kind": "modify"),该触发器就会被再次激活——哪怕此次变更由它自己发起。
在您的流程中:
- validateData.sjs 接收一份属于 processed 集合的文档;
- 执行验证后,调用 xdmp.documentInsert(uri, doc, perms, 'export-ready') —— 此操作本质是覆盖写入同一 URI 的文档;
- 尽管集合从 processed 变更为 export-ready,但文档内容本身(如 JSON 主体)可能未变;然而,MarkLogic 将 xdmp.documentInsert() 视为一次完整的文档更新操作,会重新评估所有匹配该 URI 的触发器;
- 由于原触发器监听的是 "collection-scope": {"uri": "processed"} + "update-kind": "modify",而 xdmp.documentInsert() 在覆盖时仍保留原 URI,且新文档在插入前可能仍被临时识别为 processed 集合(取决于事务上下文与集合更新时序),从而满足触发条件,造成二次调用。
✅ 正确解法:显式禁用递归
创建触发器时,必须将 recursive 参数设为 false:
// ✅ 推荐:创建 validateData 触发器时禁用递归
trgr.createTrigger(
'validate-data-trigger',
'validateData.sjs',
trgr.triggerEvent(
trgr.collectionScope('processed'),
trgr.documentContent('modify')
),
trgr.postCommit(),
fn.false() // ← 关键!显式设置 recursive = false
);
⚠️ 注意事项:
- recursive: false 仅阻止同一触发器实例对自身引发的变更做出响应,不影响其他触发器(如后续监听 export-ready 的触发器)正常工作;
- 不要依赖“跳过已验证文档”等业务逻辑规避——这无法解决底层机制问题,且易引入竞态;
- 建议对所有可能自修改文档的 post-commit 触发器统一审查 recursive 设置;
- 若需实现条件性再触发(如仅当特定字段变更时重验),应改用 trgr.documentContent('modify', ...) 配合 XPath 过滤器,而非依赖递归。
总结:MarkLogic 触发器的优雅性建立在明确契约之上。recursive=false 不是“高级选项”,而是生产环境链式 ETL 流程的安全基线配置。务必在部署前验证所有 trgr.createTrigger() 调用是否显式声明了该参数,避免隐蔽的触发风暴(trigger storm)破坏数据一致性与可观测性。










