☕ 5-Min Tech Story #13

ภาพลวงตาของ @Transactional: โค้ดพ่น Error กระจาย แต่ทำไมข้อมูลไม่ยอม Rollback?

⏱️ ใช้เวลาอ่านประมาณ: 4 นาที 15 วินาที

ในฟังก์ชันโอนเงินสุดคลาสสิก คุณเขียนคำสั่งตัดเงินจากบัญชี A แล้วสั่งโอนเข้าบัญชี B ตบท้ายด้วยการแปะ @Transactional ไว้บนหัวฟังก์ชันอย่างสบายใจ

จังหวะตัดเงินเสร็จ ระบบปลายทางพังพอดี! โค้ดพ่น Exception แดงเถือกออกมาเต็มหน้าจอ... คุณโล่งใจเพราะคิดว่า Spring คงช่วย Rollback คืนเงิน ให้บัญชี A เรียบร้อยแล้ว

แต่พอเปิดฐานข้อมูลดู... เงินถูกหักไปแล้วจริงๆ แต่เงินไม่เข้าบัญชี B! Transaction ไม่ยอม Rollback เกิดอะไรขึ้นกับเวทมนตร์ที่คุณไว้ใจมาตลอด?

🎭 ร่างเงาตัวแทน: ความลับของ Dynamic Proxy

เบื้องหลังของ Annotation ใน Spring เกือบทั้งหมดทำงานผ่านสิ่งที่เรียกว่า Proxy Pattern (ร่างตัวแทน)

เวลาคุณสั่ง Dependency Injection เจ้า Spring ไม่ได้ส่ง Class ตัวจริงของคุณไปให้คนอื่นใช้ตรงๆ แต่ส่ง "ร่างตัวแทน" ที่ห่อหุ้มโค้ดเดิมเอาไว้:

"เมื่อมีคนภายนอกเรียกฟังก์ชัน ร่างตัวแทนจะแอบแทรกคำสั่ง: connection.setAutoCommit(false) ก่อนเริ่มงาน และถ้ามีอะไรพัง ร่างตัวแทนถึงจะสั่ง rollback() ให้คุณ"

🕳️ กับดักข้อที่ 1: การคุยกันเองในบ้าน (Self-Invocation)

นี่คือจุดตายอันดับหนึ่งที่เจอใน Production บ่อยที่สุด:

@Service
class PaymentService {

  fun processPayment() {
    // เรียกฟังก์ชันข้างล่างในคลาสเดียวกันตรงๆ (this.transferMoney)
    transferMoney()
  }

  @Transactional
  fun transferMoney() {
    // โค้ดตัดเงิน... แล้วระเบิด Exception!
  }
}

ถ้าคุณเรียก processPayment() จากภายนอก แล้วข้างในแอบเรียก transferMoney() เอง การเรียกครั้งนี้จะ วิ่งเข้าตัวจริงโดยตรงผ่าน this ไม่ผ่านร่างตัวแทน Proxy!

ผลคือ: @Transactional บนหัว transferMoney() จะกลายเป็น แค่ตัวหนังสือประดับบารมีที่ไม่มีผลอะไรเลย ไม่มี Transaction ถูกเปิด และไม่มีการ Rollback ใดๆ เกิดขึ้นทั้งสิ้น!

💣 กับดักข้อที่ 2: นิสัยเลือกปฏิบัติของ Exception

หลายคนไม่รู้ว่า โดยค่าเริ่มต้น (Default) ของ Spring:

ถ้าอยากให้มันช่วย Rollback ทุกกรณีแบบไม่เลือกปฏิบัติ คุณต้องบอกมันชัดๆ เสมอ:

@Transactional(rollbackFor = [Exception::class])

💡 5-Min Takeaway

"อย่าไว้ใจเวทมนตร์ที่คุณไม่รู้ว่าเบื้องหลังมันขยับยังไง" ในโลกของ Spring การแปะ Annotation ไม่ใช่การเสกคาถา แต่เป็นการสร้าง Proxy มาครอบ ดังนั้น จำกฎ 2 ข้อนี้ให้ขึ้นใจ: อย่าเรียกฟังก์ชันที่มี Transaction จากในคลาสเดียวกันเอง และ ระบุ rollbackFor ให้ครอบคลุมเสมอ ถ้าระบบนั้นซีเรียสเรื่องเงินทอง