@Transactional ไม่ใช่แค่แปะแล้วจบ: เจาะลึก Propagation และ Rollback Rules ที่ Dev ต้องรู้

February 09, 2026 · 3 min read · 0 views

การจัดการ Database Transaction ใน Spring Boot ดูเหมือนง่าย แค่แปะ Annotation @Transactional ไว้บน Method ทุกอย่างก็น่าจะจบ... หรือเปล่า?

ความจริงคือ @Transactional มีค่า Default หลายอย่างที่ถ้าเราไม่เข้าใจกลไกของมัน อาจนำไปสู่หายนะที่ "ข้อมูลไม่ถูก Rollback เมื่อเกิด Error" หรือ "ข้อมูลถูก Commit ทั้งที่ควรจะพังไปพร้อมกัน"

บทความนี้จะพาไปดู 2 หลุมพรางใหญ่ที่ Developer มักตกม้าตายครับ

1. Rollback Rules: ทำไม Error แล้ว Transaction ไม่ยอม Rollback?

นี่คือปัญหาอันดับ 1 ที่เจอกันบ่อยที่สุด สาเหตุมาจากกฎการแบ่งแยก Exception ใน Java

กฎ Default ของ Spring Transaction

Spring จะทำการ Rollback Transaction อัตโนมัติ เฉพาะ กรณีที่เกิด Runtime Exception (Unchecked) หรือ Error เท่านั้น

  • NullPointerException: Rollback
  • IllegalArgumentException: Rollback
  • IOException: ไม่ Rollback (เพราะเป็น Checked Exception)
  • SQLException: ไม่ Rollback (ถ้าเป็น Checked Version)
  • Exception (General): ไม่ Rollback

ตัวอย่างความผิดพลาด (The Pitfall)

สมมติเรามี Method ที่ต้องบันทึกข้อมูลและอ่านไฟล์ ถ้าอ่านไฟล์ไม่เจอ เราอยากให้ยกเลิกการบันทึกข้อมูล

java
@Transactional
public void saveAndReadFile() throws IOException {
    userRepository.save(user); // 1. บันทึก User ลง DB
    
    // 2. จำลองอ่านไฟล์แล้วพัง (Throw Checked Exception)
    throw new IOException("File not found"); 
}

ผลลัพธ์: Transaction Commited! User ถูกบันทึกลง Database ไปแล้ว ทั้งที่ Logic โดยรวมพัง เพราะ IOException เป็น Checked Exception ซึ่งอยู่นอกเหนือการจัดการ Default ของ Spring

วิธีแก้ไข (The Fix)

เราต้องบอก Spring อย่างชัดเจนว่าให้ Rollback ทุกกรณีไม่ว่าเป็น Exception ประเภทไหน โดยใช้ Parameter rollbackFor

java
// สั่งให้ Rollback เมื่อเจอ Exception ทุกประเภท
@Transactional(rollbackFor = Exception.class)
public void saveAndReadFile() throws Exception {
    // ... code ...
}

Best Practice: แนะนำให้ใส่ rollbackFor = Exception.class ไว้เสมอ หาก Business Logic ของคุณมีความเสี่ยงที่จะเกิด Checked Exception


2. Propagation: เมื่อ Transaction ซ้อน Transaction

เมื่อ Method A เรียก Method B และทั้งคู่มี @Transactional ทั้งคู่ Transaction จะทำงานร่วมกันอย่างไร? นี่คือเรื่องของ Propagation

Default: REQUIRED (ลงเรือลำเดียวกัน)

หากเราไม่กำหนดค่าอะไร Default คือ Propagation.REQUIRED

java
@Transactional // A เริ่ม Transaction
public void methodA() {
    methodB(); 
}

@Transactional // B ตรวจเจอว่ามี Transaction อยู่แล้ว ก็จะเข้าร่วม (Join)
public void methodB() {
    // ...
}
  • พฤติกรรม: ทั้ง A และ B ใช้ Connection เดียวกัน
  • ผลลัพธ์: ถ้า B พัง A ก็พังด้วย (Rollback ทั้งหมด)

REQUIRES_NEW (แยกวง)

มีบางกรณีที่เราต้องการให้ B ทำงานสำเร็จและ Commit ไปเลย แม้ว่า A จะพังในภายหลัง เช่น การบันทึก Audit Log หรือ Notification

java
@Transactional(propagation = Propagation.REQUIRES_NEW)
public void saveAuditLog() {
    // Spring จะ Suspend Transaction เก่า
    // และสร้าง Transaction ใหม่เพื่อ method นี้โดยเฉพาะ
    auditRepository.save(log);
}
  • พฤติกรรม: แยก Connection, แยก Transaction
  • ผลลัพธ์: ถ้า saveAuditLog ทำงานเสร็จ มันจะ Commit ทันที ต่อให้ Method หลักที่เรียกมันมาจะ Error ในบรรทัดถัดไป ข้อมูล Log ก็ยังคงอยู่

3. กับดักพิเศษ: Self-Invocation (เรียกตัวเองไม่ได้นะ)

เรื่องนี้สำคัญมากและแก้ยากถ้าไม่เข้าใจโครงสร้าง Spring AOP

@Transactional ทำงานผ่านกลไก Proxy (ตัวกลาง) เมื่อเราเรียก Method จากภายนอก Class มันจะวิ่งผ่าน Proxy -> เริ่ม Transaction -> เข้า Method จริง

แต่! ถ้าเราเรียก Method ภายใน Class เดียวกัน (Self-Invocation) มันจะเป็นการเรียก this.method() ตรงๆ โดย ไม่ผ่าน Proxy

java
@Service
public class UserService {

    public void mainMethod() {
        // การเรียกแบบนี้ Transaction จะไม่ทำงาน!!
        // เพราะเป็นการเรียกภายใน instance เดียวกัน ไม่ผ่าน Proxy
        doTransactionWork(); 
    }

    @Transactional
    public void doTransactionWork() {
        // Logic DB...
    }
}

วิธีแก้:

  1. ย้าย doTransactionWork ไปไว้ใน Service อื่น (Clean สุด)
  2. Inject ตัวเองเข้ามา (Self-Inject) แต่ต้องระวัง Circular Dependency

สรุป

การใช้ @Transactional ให้ปลอดภัย:

  1. Check Exception Type: หากมี Checked Exception ให้ใช้ @Transactional(rollbackFor = Exception.class) เสมอ
  2. Understand Context: หากต้องการแยกการทำงานออกจาก Transaction หลัก ให้ใช้ Propagation.REQUIRES_NEW
  3. Beware Self-Call: อย่าเรียก Transactional Method จาก Method อื่นใน Class เดียวกัน

เข้าใจ 3 ข้อนี้ ข้อมูลใน Database ของคุณจะปลอดภัยและ Consistent ขึ้นแน่นอนครับ

Responses (0)

No responses yet. Be the first to respond.