ภาพลวงตาของ @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 บ่อยที่สุด:
class PaymentService {
fun processPayment() {
// เรียกฟังก์ชันข้างล่างในคลาสเดียวกันตรงๆ (this.transferMoney)
transferMoney()
}
@Transactional
fun transferMoney() {
// โค้ดตัดเงิน... แล้วระเบิด Exception!
}
}
ถ้าคุณเรียก processPayment() จากภายนอก แล้วข้างในแอบเรียก
transferMoney() เอง การเรียกครั้งนี้จะ
วิ่งเข้าตัวจริงโดยตรงผ่าน this ไม่ผ่านร่างตัวแทน
Proxy!
ผลคือ: @Transactional บนหัว
transferMoney() จะกลายเป็น
แค่ตัวหนังสือประดับบารมีที่ไม่มีผลอะไรเลย ไม่มี
Transaction ถูกเปิด และไม่มีการ Rollback ใดๆ เกิดขึ้นทั้งสิ้น!
💣 กับดักข้อที่ 2: นิสัยเลือกปฏิบัติของ Exception
หลายคนไม่รู้ว่า โดยค่าเริ่มต้น (Default) ของ Spring:
-
จะ Rollback ให้เฉพาะ
Unchecked Exception (เช่น
RuntimeExceptionหรือError) เท่านั้น -
ถ้าคุณ
throw Exceptionทั่วไป หรือIOExceptionที่เป็น Checked Exception... Spring จะถือว่า "อ๋อ ข้อผิดพลาดที่คุณคาดไว้อยู่แล้วเหรอ งั้นกู Commit ปกตินะ!"
ถ้าอยากให้มันช่วย Rollback ทุกกรณีแบบไม่เลือกปฏิบัติ คุณต้องบอกมันชัดๆ เสมอ:
💡 5-Min Takeaway
"อย่าไว้ใจเวทมนตร์ที่คุณไม่รู้ว่าเบื้องหลังมันขยับยังไง"
ในโลกของ Spring การแปะ Annotation ไม่ใช่การเสกคาถา แต่เป็นการสร้าง
Proxy มาครอบ ดังนั้น จำกฎ 2 ข้อนี้ให้ขึ้นใจ:
อย่าเรียกฟังก์ชันที่มี Transaction จากในคลาสเดียวกันเอง และ
ระบุ rollbackFor ให้ครอบคลุมเสมอ
ถ้าระบบนั้นซีเรียสเรื่องเงินทอง