Transaction management


Spring Boot দিয়ে কাজ করার সময় আমরা অনেক সময় database transaction নিয়ে কাজ করি — যেমন একই সময়ে একাধিক টেবিলে ডেটা ইনসার্ট, আপডেট বা ডিলিট করতে হয়। কিন্তু মাঝপথে কোনো ত্রুটি ঘটলে, কিভাবে সব পরিবর্তন rollback হবে? এখানেই আসে Transaction Management


Transaction কী?

একটি Transaction হলো ডাটাবেজের একটি অপারেশনের ইউনিট যা সবগুলো ধাপ সফলভাবে সম্পন্ন হলে তবেই commit হয়, অন্যথায় rollback হয়।
এর মানে হলো — আংশিকভাবে কোনো পরিবর্তন ডাটাবেজে যাবে না।

উদাহরণ:

1️⃣ ব্যাংক A থেকে 100 টাকা কমাও  
2️⃣ ব্যাংক B তে 100 টাকা বাড়াও  

যদি প্রথম ধাপ সফল হয়, কিন্তু দ্বিতীয় ধাপে ত্রুটি ঘটে — তাহলে পুরো Transaction rollback হবে, এবং কোনো পরিবর্তনই থাকবে না।


Spring Boot এ Transaction Management কিভাবে কাজ করে?

Spring Boot এ Transaction পরিচালনা করতে আমরা সাধারণত @Transactional annotation ব্যবহার করি। এটি Spring Framework কে বলে দেয় যে এই মেথডের সব ডাটাবেজ অপারেশন একটি Transaction এর মধ্যে চলবে।


Service Layer এ Transaction ব্যবহার

@Service
public class TransactionService {

    @Autowired
    private AccountRepository accountRepository;

    @Transactional
    public void transferMoney(Long fromId, Long toId, double amount) {
        Account fromAccount = accountRepository.findById(fromId)
                .orElseThrow(() -> new RuntimeException("From account not found"));
        Account toAccount = accountRepository.findById(toId)
                .orElseThrow(() -> new RuntimeException("To account not found"));

        fromAccount.setBalance(fromAccount.getBalance() - amount);
        toAccount.setBalance(toAccount.getBalance() + amount);

        accountRepository.save(fromAccount);

        // উদাহরণস্বরূপ ইচ্ছাকৃতভাবে একটি exception ছুঁড়ে দেওয়া হলো
        if (amount > 5000) {
            throw new RuntimeException("Transfer limit exceeded!");
        }

        accountRepository.save(toAccount);
    }
}

এখানে @Transactional নিশ্চিত করে যে যদি কোনো exception ঘটে, তাহলে পুরো ট্রান্সফার rollback হবে।


Rollback কিভাবে কাজ করে?

Spring Boot স্বয়ংক্রিয়ভাবে RuntimeException ঘটলে Transaction rollback করে দেয়।
তবে তুমি চাইলে নির্দিষ্ট Exception এর জন্য rollback নির্ধারণ করতে পারো:

@Transactional(rollbackFor = Exception.class)


Transaction Propagation

Spring Transaction propagation নির্ধারণ করে কোন transaction context ব্যবহার হবে, যখন একটি transactional method অন্য আরেকটি transactional method কে কল করে।

Mode ব্যাখ্যা
REQUIRED (default) আগের transaction থাকলে সেটাই ব্যবহার হবে, না থাকলে নতুন তৈরি করবে।
REQUIRES_NEW আগের transaction suspend হবে, নতুন transaction শুরু হবে।
NESTED একটি transaction এর মধ্যে nested transaction তৈরি করে। rollback হলে parent safe থাকে।
SUPPORTS transaction থাকলে join করে, না থাকলে non-transactional ভাবে চলে।
MANDATORY transaction না থাকলে exception ছুঁড়ে দেয়।
NEVER transaction থাকলে exception ছুঁড়ে দেয়।
NOT_SUPPORTED সবসময় non-transactional মোডে কাজ করে।


Example:

@Transactional(propagation = Propagation.REQUIRES_NEW) public void logTransactionHistory(String message) { historyRepository.save(new History(message)); }

এখানে মূল transaction suspend হবে, এবং লগ রেকর্ড সবসময় আলাদা transaction এ save হবে।


Transaction Isolation Levels

একাধিক concurrent transaction চললে ডাটার consistency রক্ষা করা জরুরি। এজন্য ব্যবহৃত হয় isolation level

LevelPreventsপারফরম্যান্স প্রভাব
READ_UNCOMMITTEDকোনোটি নাদ্রুত কিন্তু unsafe
READ_COMMITTEDDirty Readsafe এবং সাধারণত default
REPEATABLE_READDirty, Non-Repeatable Readconsistent কিন্তু ধীর
SERIALIZABLEসব রিড সমস্যা প্রতিরোধসবচেয়ে নিরাপদ, কিন্তু সবচেয়ে ধীর


Example:

@Transactional(isolation = Isolation.REPEATABLE_READ) public List<Account> getAccounts() { return accountRepository.findAll(); }


 Rollback Rules কাস্টমাইজ করা

Spring Boot ডিফল্টভাবে শুধু RuntimeException ঘটলে rollback করে।


তবে তুমি চাইলে নির্দিষ্ট Exception এর জন্যও rollback enforce করতে পারো:

@Transactional(rollbackFor = { SQLException.class, IOException.class })


অন্যদিকে, কোনো Exception এ rollback না চাইলে:

@Transactional(noRollbackFor = CustomBusinessException.class)


Read-only Transaction

যদি তোমার মেথড কেবল read/query করে, কোনো পরিবর্তন না আনে, তাহলে readOnly = true ব্যবহার করো।
এতে পারফরম্যান্স বাড়ে এবং Hibernate কিছু অপ্টিমাইজেশন প্রয়োগ করে।

@Transactional(readOnly = true) public List<User> findAllUsers() { return userRepository.findAll(); }


Nested Transactions

Propagation.NESTED ব্যবহার করলে parent transaction এর ভিতরে savepoint তৈরি হয়।
যদি child transaction ব্যর্থ হয়, parent rollback হবে না — savepoint পর্যন্ত rollback হবে।

@Transactional(propagation = Propagation.NESTED) public void updatePartialData() { // কিছু আপডেট // ব্যর্থ হলে শুধু এই অংশ rollback হবে }

নোট: Nested transaction সাপোর্ট পেতে datasource কে DataSourceTransactionManager হতে হবে; JPA এর JpaTransactionManager পুরোপুরি nested support দেয় না।



Transaction Management Best Practices

- Service Layer এ @Transactional ব্যবহার করো, Controller এ নয়।

- @Transactional method যেন public হয় (Spring proxy private method handle করে না)।

- Lazy-loading সমস্যা এড়াতে Entity fetch যতটা সম্ভব service layer এর মধ্যেই সম্পন্ন করো।

- Read-only queries এর জন্য readOnly = true ব্যবহার করো।

- Exception handling করলে, RuntimeException পুনরায় throw করতে ভুলো না — না হলে rollback হবে না।



Practical Example

@Service public class OrderService { @Autowired private OrderRepository orderRepo; @Autowired private PaymentService paymentService;
@Transactional public void placeOrder(Order order) { orderRepo.save(order); paymentService.processPayment(order); // যদি এখানে exception হয় → rollback } } @Service public class PaymentService { @Transactional(propagation = Propagation.REQUIRES_NEW) public void processPayment(Order order) { // নতুন transaction এ পেমেন্ট প্রসেস } }

এখানে OrderService.placeOrder() ব্যর্থ হলে পুরো order rollback হবে,
কিন্তু PaymentService এর REQUIRES_NEW propagation থাকার কারণে তার transaction unaffected থাকবে।


Common Pitfalls

Self Invocation Issue
যদি একই ক্লাসের ভেতর একটি @Transactional method অন্যটি কল করে,
Spring proxy সেটিকে intercept করতে পারে না, ফলে transaction কাজ করবে না।
সমাধান: transaction logic আলাদা service ক্লাসে রাখো।

Checked Exception rollback না হওয়া
Spring শুধুমাত্র RuntimeException এ rollback করে।
সমাধান: rollbackFor = Exception.class যুক্ত করো।