สิ่งที่ Claude คิดว่าตัวเองรู้เกี่ยวกับ Rails Callbacks

ผมพยายามรัน rake task เพื่อลบ record ของ LineItem และไฟล์ใน S3 ของมัน ผมต้องการหลีกเลี่ยง callback ที่กินทรัพยากรสูง (expensive callbacks) ใน model พ่ออย่าง Order

ผมถาม Claude ให้ช่วย มันให้คำตอบที่ดูมั่นใจมาก แต่มันผิด

และนี่คือสิ่งที่ผมได้เรียนรู้เกี่ยวกับ Rails, counter caches และเหตุผลที่คุณต้องตรวจสอบคำแนะนำจาก AI เสมอ

ปัญหาที่พบ LineItem belongs to OrderItem และ OrderItem belongs to Order ทั้งคู่มีการใช้ counter_cache และ touch การลบ LineItem จะไปกระตุ้นให้เกิด cascade (การทำงานต่อเนื่องเป็นทอดๆ) ซึ่ง cascade นี้จะไปสั่งรัน job หนักๆ เช่น การประมาณการค่าขนส่งและการคำนวณยอดรวมใหม่ ผมจำเป็นต้องหยุด cascade นี้เพื่อประหยัด CPU และค่าใช้จ่าย S3

ความผิดพลาดของ AI Claude แนะนำให้ใช้ skip_callback ซึ่งเป็นความคิดที่ไม่ดีเลย เพราะ skip_callback จะไปแก้ไข class ในระดับ global ซึ่งส่งผลกระทบต่อทุก thread ในแอปของคุณ หากโค้ดเกิด crash ก่อนที่คุณจะเปิดใช้งานมันอีกครั้ง callback ของคุณก็จะหายไปถาวร

จากนั้นผมลองใช้ no_touching โดยผมครอบการเรียกใช้งานทั้งใน OrderItem และ Order เพื่อความปลอดภัย ผลการทดสอบผ่าน แต่ใน console กลับแสดงผลต่างออกไป คือ timestamp ของ Order ยังคงเปลี่ยนอยู่

สาเหตุที่แท้จริง ปัญหามันอยู่ที่วิธีการทำงานร่วมกันระหว่าง counter_cache และ touch

  • เมื่อคุณใช้ counter_cache: true และ touch: true ร่วมกัน Rails จะรวมการทำงานเข้าด้วยกัน
  • มันจะรันคำสั่ง raw SQL UPDATE ALL เพียงคำสั่งเดียว
  • Raw SQL จะข้าม ActiveRecord lifecycle ไปเลย
  • และเพราะมันข้าม lifecycle ไป ทำให้ hook after_commit ไม่ทำงาน

สิ่งนี้ทำให้เกิดความย้อนแย้งที่แปลกประหลาด:

  • callback ของ OrderItem ไม่ทำงานเพราะการรวมคำสั่งเป็น raw SQL
  • แต่ callback ของ Order กลับทำงาน เพราะขั้นตอนถัดไปใน chain คือการ touch แบบธรรมดา

วิธีแก้ไข ผมแค่ต้องครอบ "รุ่นปู่" (grandparent) ด้วย no_touching เท่านั้น

Order.no_touching { line_item.destroy! }

วิธีนี้จะหยุดการ touch ในระดับ AR ไม่ให้ส่งไปถึง model Order มันไม่ได้หยุด raw SQL ใน OrderItem แต่ก็ไม่สำคัญ เพราะการรวมคำสั่ง counter cache ได้ข้าม callback เหล่านั้นไปเรียบร้อยแล้ว

บทเรียนสำคัญ

  • counter_cache: true + touch: true = raw SQL UPDATE ALL
  • Raw SQL จะข้าม after_commit hooks ทั้งหมด
  • การ touch แบบธรรมดา (ที่ไม่มี counter cache) จะทำงานตามมาตรฐาน AR lifecycle และเรียกใช้งาน callback ตามปกติ
  • อย่าเชื่อโค้ดจาก AI อย่างหลับหูหลับตา Claude แค่อยากจะให้คำตอบแก่คุณ แต่มันไม่ได้สนใจว่าคำตอบนั้นจะเป็นการมโน (hallucination) หรือไม่

ทดสอบสมมติฐานของคุณใน Rails console เสมอ ลองทำให้โค้ดพังด้วยความตั้งใจ เพื่อดูว่ามันสามารถหยุดพฤติกรรมที่คุณต้องการบล็อกได้จริงหรือไม่

Source: https://dev.to/husteadrobert/what-claude-thought-he-knew-about-rails-callbacks-and-how-console-testing-proved-him-wrong-j20

Optional learning community: https://t.me/GyaanSetuAi