𝗪𝗵𝗮𝘁 𝗖𝗹𝗮𝘂𝗱𝗲 𝗧𝗵𝗼𝘂𝗴𝗵𝘁 𝗛𝗲 𝗞𝗻𝗲𝘄 𝗔𝗯𝗼𝘂𝘁 𝗥𝗮𝗶𝗹𝘀 𝗖𝗮𝗹𝗹𝗯𝗮𝗰𝗸𝘀
I tried to run a rake task to delete LineItem records and their S3 files. I wanted to avoid expensive callbacks on parent models like Order.
I asked Claude for help. It gave me a confident answer. It was wrong.
Here is what I learned about Rails, counter caches, and why you must verify AI advice.
𝗧𝗵𝗲 𝗣𝗿𝗼𝗯𝗹𝗲𝗺 LineItem belongs to OrderItem. OrderItem belongs to Order. Both use counter_cache and touch. Deleting a LineItem triggers a cascade. This cascade fires heavy jobs like shipping estimations and total recalculations. I needed to stop this cascade to save CPU and S3 costs.
𝗧𝗵𝗲 𝗔𝗜 𝗠𝗶𝘀𝘁𝗮𝗸𝗲 Claude suggested using skip_callback. This is a bad idea. skip_callback modifies the class globally. It affects every thread in your app. If your code crashes before you re-enable it, your callbacks stay dead.
I then tried no_touching. I wrapped the call in both OrderItem and Order to be safe. The tests passed, but the console showed something different. The Order timestamp still changed.
𝗧𝗵𝗲 𝗥𝗲𝗮𝗹 𝗥𝗲𝗮𝘀𝗼𝗻 The issue was how counter_cache works with touch.
- When you use counter_cache: true and touch: true together, Rails bundles them.
- It runs a single raw SQL UPDATE ALL command.
- Raw SQL bypasses the ActiveRecord lifecycle.
- Because it bypasses the lifecycle, after_commit hooks do not fire.
This created a weird paradox:
- OrderItem callbacks did not fire because of the raw SQL bundle.
- Order callbacks DID fire because the next hop in the chain was a plain touch.
𝗧𝗵𝗲 𝗙𝗶𝘅 I only needed to wrap the grandparent in no_touching.
Order.no_touching { line_item.destroy! }
This stops the AR-level touch from reaching the Order model. It does not stop the raw SQL on OrderItem, but that does not matter because the counter cache bundle already skips those callbacks.
𝗞𝗲𝘆 𝗧𝗮𝗸𝗲𝗮𝘄𝗮𝘆𝘀
- counter_cache: true + touch: true = raw SQL UPDATE ALL.
- Raw SQL skips all after_commit hooks.
- A plain touch (without a counter cache) follows the standard AR lifecycle and fires callbacks.
- Never trust AI code blindly. Claude wants to give you an answer. It does not care if that answer is a hallucination.
Always test your assumptions in the Rails console. Break the code on purpose to see if it actually stops the behavior you want to block.
Optional learning community: https://t.me/GyaanSetuAi