Claude 对 Rails 回调的错误认知
我尝试运行一个 rake 任务来删除 LineItem 记录及其 S3 文件。我想避免在 Order 等父模型上触发昂贵的 callback。
我向 Claude 求助。它给出了一个充满自信的回答。但它是错的。
以下是我关于 Rails、counter_cache 以及为什么必须验证 AI 建议所学到的经验。
问题所在
LineItem 属于 OrderItem。OrderItem 属于 Order。两者都使用了 counter_cache 和 touch。
删除一个 LineItem 会触发级联反应。这种级联会触发诸如运费估算和总额重新计算之类的重型任务。
我需要停止这种级联,以节省 CPU 和 S3 成本。
AI 的错误
Claude 建议使用 skip_callback。
这是一个坏主意。skip_callback 会全局修改类。它会影响应用中的每一个线程。如果你的代码在重新启用它之前崩溃了,你的 callback 将会一直处于失效状态。
然后我尝试了 no_touching。为了保险起见,我在 OrderItem 和 Order 的调用中都包裹了它。
测试通过了,但控制台显示的结果却不同。Order 的时间戳仍然发生了变化。
真实原因
问题在于 counter_cache 与 touch 的协作方式。
- 当你同时使用
counter_cache: true和touch: true时,Rails 会将它们捆绑在一起。 - 它会运行一条单一的原始 SQL
UPDATE ALL命令。 - 原始 SQL 会绕过 ActiveRecord 的生命周期。
- 因为绕过了生命周期,
after_commit钩子不会触发。
这产生了一个奇怪的悖论:
- 由于原始 SQL 的捆绑执行,
OrderItem的 callback 没有触发。 - 但
Order的 callback 确实触发了,因为链条中的下一步是一个普通的touch。
解决方案
我只需要在“祖父级”模型上包裹 no_touching 即可。
Order.no_touching { line_item.destroy! }
这阻止了 AR 级别的 touch 传递到 Order 模型。它不会阻止 OrderItem 上的原始 SQL,但这并不重要,因为 counter_cache 的捆绑操作已经跳过了那些 callback。
核心启示
counter_cache: true+touch: true= 原始 SQLUPDATE ALL。- 原始 SQL 会跳过所有的
after_commit钩子。 - 普通的
touch(不带counter_cache)遵循标准的 AR 生命周期并触发 callback。 - 永远不要盲目信任 AI 代码。Claude 只是想给你一个答案,它并不关心这个答案是否是幻觉。
务必在 Rails 控制台中测试你的假设。故意破坏代码,看看它是否真的停止了你想要阻止的行为。
Optional learning community: https://t.me/GyaanSetuAi