สิ่งที่ 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 SQLUPDATE ALL- Raw SQL จะข้าม
after_commithooks ทั้งหมด - การ
touchแบบธรรมดา (ที่ไม่มี counter cache) จะทำงานตามมาตรฐาน AR lifecycle และเรียกใช้งาน callback ตามปกติ - อย่าเชื่อโค้ดจาก AI อย่างหลับหูหลับตา Claude แค่อยากจะให้คำตอบแก่คุณ แต่มันไม่ได้สนใจว่าคำตอบนั้นจะเป็นการมโน (hallucination) หรือไม่
ทดสอบสมมติฐานของคุณใน Rails console เสมอ ลองทำให้โค้ดพังด้วยความตั้งใจ เพื่อดูว่ามันสามารถหยุดพฤติกรรมที่คุณต้องการบล็อกได้จริงหรือไม่
Optional learning community: https://t.me/GyaanSetuAi