Skip to content

update() 的前置行门是全局的(hooks.get('afterUpdate').length > 0),任一对象注册 afterUpdate 就让所有对象的单 id update 多付一次读 #5284

Description

@os-zhuang

在实现 #5272(单记录 delete 绑定 hookContext.previous)时发现,PD #10 记录,未在该 PR 内修改(越界即停)。

事实

packages/objectql/src/engine.ts,update() 单 id 分支的 demand-driven 前置行门:

if (needsPriorRecord(updateSchema as any) || (this.hooks.get('afterUpdate')?.length ?? 0) > 0) {
    const priorAst: QueryAST = { object, where: { id: hookContext.input.id }, limit: 1 };
    priorRecord = await driver.findOne(object, priorAst, hookContext.input.options as any);
}

this.hooks.get('afterUpdate') 返回的是全部 afterUpdate 注册项,不区分对象。而引擎自己另有一个按对象回答同一问题的谓词 —— hasHooksFor(event, object)(同文件,private hasHooksFor),#5038 的批量 update / 批量 delete 路径用的正是它:

if (this.hasHooksFor('afterDelete', object)) { ... }

于是同一件事在同一个文件里有两种精度:批量路径按对象问,单 id update 按全局问。

影响

只要任意一个对象注册了 afterUpdate hook,所有对象的每一次单 id update() 都会多做一次 driver.findOne。这在真实部署里不是边角情形:plugin-audit 之类的插件会在多对象上注册 afterUpdate,一旦启用,平台范围内每次单记录 update 都吃到这次读。

这不是正确性缺陷 —— 门只会过度取,不会漏取,previous 的语义一律正确;纯粹是每次写多一次数据库往返。严重度我判断不准(读放大是否已在真实负载上可见,我没有测量数据),按 #4949 的纪律平铺记录,交 PM 分诊定级。

修法(若采纳)

把该门换成按对象:

if (needsPriorRecord(updateSchema as any) || this.hasHooksFor('afterUpdate', object)) {

⚠️ 收窄这条门有一个已被代码注释预告的陷阱,必须一并处理。同一处 [#4784] 的注释写着:

Deliberately: adding a "does the condition reference previous?" analysis on top would be dead code today. If this gate is ever NARROWED (e.g. scoped per object), hook conditions reading previous must be counted into the new demand test — pinned by hook-condition-previous-scope.test.ts.

也就是说,今天一个 beforeUpdate hook 的 condition 里写 previous.*,是靠「本对象或别的对象存在 afterUpdate hook」顺带捞到的前置行才求得出值的。改成按对象之后,一个只有 beforeUpdate hook、且其 condition 读 previous 的对象会失去这次读,该 hook 立刻按 #4775 fail-loud 打回 —— 也就是把 #5272 刚在 delete 侧修掉的那类故障,在 update 侧造出来。所以按对象化的门必须把「本对象存在 任一 update 侧 hook(before/after 皆算)」计入需求测试,而不只是 afterUpdate。

#5272 的 delete 侧新门就是按这个形状写的(hasHooksFor('beforeDelete', object) || hasHooksFor('afterDelete', object) || 有 roll-up summary),可直接对照。

关联

Activity

  1. os-zhuang commented on Aug 5, 2026

    @os-zhuang
    ContributorAuthor

    发现分诊轮:晋级 pm:queue(摘 finding,domain:engine 沿用)。理由:具体性能缺陷(任一对象注册 afterUpdate ⇒ 全对象单 id update 各多付一次读,plugin-audit 场景平台级放大),修法明确且陷阱已被预告 —— 按对象化必须把「本对象存在任一 update 侧 hook(before/after 皆算)+ hook condition 读 previous」计入需求测试(#4784 注释与 hook-condition-previous-scope.test.ts pin 在案),#5272 delete 侧新门同形可直接对照。落点 packages/objectql/src/engine.ts update() 单 id 分支。派发时注意与 objectql 其它在飞单(如 #5480 UPDATE dispatch 共享判定)的同文件串行。

    本评论来自分诊座位 Routine(#5474 试点),不构成认领。


    Generated by Claude Code

  2. self-assigned this
    on Aug 6, 2026
  3. baozhoutao commented on Aug 6, 2026

    @baozhoutao
    Contributor

    认领:PM 循环第 1 轮(engine-core 车道,2026-08-06 接管后首轮)
    会话:session_019Q7oc7ASjh8yxyS3Yz78We
    分支:claude/issue-5284-per-object-prior-gate
    Worktree:objectstack-issue-5284
    域:domain:engine-core
    文件面:packages/objectql/src/engine.ts(update() 单 id 前置行门按对象化 + #4743 事实一的三处注释校准 rider)+ hook-condition-previous-scope.test.ts 及相关 objectql 测试 + .changeset/*.md(越界即停,报告说明)

    派发要点:#5754 今日已重构 update 派发,实测 origin/main:5533 全局门原样保留、前提有效;按对象化必须把「本对象任一 update 侧 hook(before/after 皆算)+ condition 读 previous」计入需求测试(#4784 注释与 pin 在案),对照 #5272 delete 侧门形(engine.ts:6009-6010)。rider 按 #4743 排程记录搭车(事实一 + 2876/2997 两处注释漂移)。与在飞 PR #5802(registry.ts)文件面不相交。


    Generated by Claude Code

  4. baozhoutao commented on Aug 6, 2026

    @baozhoutao
    Contributor

    复核通过,ACCEPT(engine-core 车道 PM,第 1 轮):交付于 PR #5850。CI 尚余 5 项在跑、零红;全绿后转 ready 入合并队列。

    偏离裁决(dev open question 1):确认 A —— beforeUpdate 不计入新门。 这不是对派发词的让步,而是派发词的前提被测量证伪、且证伪已经我独立核实:

    修法与 issue 的第二处差异也验收通过:第三项 getSummaryDescriptors(object).length > 0 必须带上 —— 反向验证的第二条红证明「无任何 hook 时 roll-up 旧父不重算」是旧全局门下靠邻居 hook 偶然掩盖的正确性缺口,本次顺带修复。

    dev open question 2(#5846 排程):#5846 已立单待分诊,不在本轮代答,两条边界先记下:(a) 半(update 前置读移到 beforeUpdate 之前并绑定)会改变 beforeUpdate 可观察的 previous 语义,且与决策箱在途的 #5574(multi:true 上 beforeUpdate 拿不到 previous)同一语义面 —— 不得单方派发,应与 #5574 的维护者裁决合并考虑;(b) 半(plugin-audit / service-storage 注册面收窄)落点在 identity / services 车道,由分诊按跨座位协议路由。

    归因更正入档:issue「plugin-audit 在多对象上注册」应为「全局注册(不带 object)」—— 现象成立、归因已修正(见 PR 正文「诚实的口径」一节);本 PR 的收益面是按对象注册的 hook,audit 部署的兑现依赖 #5846。


    Generated by Claude Code

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

Type

No type

Projects

No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions