আমরা যখন Spring অ্যাপ্লিকেশন ডেভেলপ করি, তখন ডেটাবেস অপারেশনের সঠিকতা নিশ্চিত করা খুবই গুরুত্বপূর্ণ। একটি মেথড কল করার সময় যদি ডেটাবেস অপারেশন ব্যর্থ হয়, তাহলে পুরো অপারেশনটিকে রোলব্যাক করে দেওয়া উচিত। কিন্তু কখনো কখনো আমাদের প্রয়োজন হয় যে, একটি নির্দিষ্ট অপারেশন ব্যর্থ হলেও তার আগের কিছু অংশ (যেমন লগিং বা হিস্টোরি সেভ) যাতে কমিট থেকে যায়। এই জটিলতা সমাধানের জন্যই Spring-এর @Transactional অ্যানোটেশনের propagation অ্যাট্রিবিউট ব্যবহার করা হয়। এই টিউটোরিয়ালে আমরা propagation-এর বিভিন্ন মোড (যেমন REQUIRED, REQUIRES_NEW, MANDATORY ইত্যাদি) কীভাবে কাজ করে, তা ধাপে ধাপে বুঝব এবং একটি সম্পূর্ণ Spring Boot উদাহরণ দেখব।

ভূমিকা (Background)

Spring Framework-এ @Transactional অ্যানোটেশন ডেটাবেস ট্রানজ্যাকশন ম্যানেজমেন্টের মূল চাবিকাঠি। সাধারণত, একটি @Transactional মেথড কল করলে একটি নতুন ট্রানজ্যাকশন শুরু হয় এবং মেথড শেষে সেটি কমিট বা রোলব্যাক হয়। কিন্তু যখন একাধিক @Transactional মেথড একে অপরকে কল করে, তখন তাদের মধ্যে ট্রানজ্যাকশনের আচরণ কেমন হবে? এই আচরণই নির্ধারণ করে propagation অ্যাট্রিবিউট।

Propagation মূলত নির্ধারণ করে যে, একটি ট্রানজ্যাকশনাল মেথড যখন অন্য একটি ট্রানজ্যাকশনাল মেথডকে কল করে, তখন কলকারী এবং কলিত মেথডের মধ্যে ট্রানজ্যাকশনের সম্পর্ক কেমন হবে। উদাহরণস্বরূপ, একটি সার্ভিস মেথড যদি আরেকটি সার্ভিস মেথডকে কল করে, তাহলে কি তারা একই ট্রানজ্যাকশনের অংশ হবে, নাকি আলাদা আলাদা ট্রানজ্যাকশন তৈরি করবে?

Spring-এ সাতটি প্রধান propagation মোড আছে:

  • REQUIRED (ডিফল্ট): কলকারীর ট্রানজ্যাকশন থাকলে তাতে যোগ দেয়, না থাকলে নতুন তৈরি করে।
  • REQUIRES_NEW: সবসময় একটি নতুন, স্বাধীন ট্রানজ্যাকশন তৈরি করে।
  • MANDATORY: কলকারীর ট্রানজ্যাকশন থাকতে হবে, না থাকলে exception ছুড়ে।
  • NESTED: কলকারীর ট্রানজ্যাকশনের মধ্যে একটি সেভপয়েন্ট তৈরি করে।
  • SUPPORTS: কলকারীর ট্রানজ্যাকশন থাকলে তাতে যোগ দেয়, না থাকলে ট্রানজ্যাকশন ছাড়া চলে।
  • NOT_SUPPORTED: সবসময় ট্রানজ্যাকশন ছাড়া চলে।
  • NEVER: কলকারীর ট্রানজ্যাকশন থাকলে exception ছুড়ে।

এই টিউটোরিয়ালে আমরা REQUIRED, REQUIRES_NEW, এবং MANDATORY-এর উপর বেশি ফোকাস করব, কারণ এগুলোই সবচেয়ে বেশি ব্যবহৃত হয়।

পূর্বশর্ত (Prerequisites)

এই টিউটোরিয়ালটি অনুসরণ করার জন্য আপনার নিম্নলিখিত বিষয়গুলো জানা থাকা দরকার:

  • Java 8 বা তার উপরে সংস্করণের জ্ঞান।
  • Spring Boot এর মৌলিক ধারণা, যেমন @SpringBootApplication এবং @Service অ্যানোটেশন।
  • Maven বা Gradle বিল্ড টুলের ব্যবহার জানা।
  • একটি ডেটাবেস (যেমন H2 বা MySQL) এবং spring-boot-starter-data-jpa ডিপেন্ডেন্সি সেটআপ করা।
  • IDE (যেমন IntelliJ IDEA বা Eclipse) ব্যবহারে অভিজ্ঞতা।

আপনার pom.xml ফাইলে নিচের ডিপেন্ডেন্সিগুলো যোগ করুন:

<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-data-jpa</artifactId>
</dependency>
<dependency>
    <groupId>com.h2database</groupId>
    <artifactId>h2</artifactId>
    <scope>runtime</scope>
</dependency>

ধাপে ধাপে বাস্তবায়ন (Step-by-Step Implementation)

১. একটি সাধারণ Spring Boot প্রোজেক্ট তৈরি করা

প্রথমে একটি Spring Boot প্রোজেক্ট তৈরি করুন। আমরা একটি User এন্টিটি এবং একটি UserService ক্লাস ব্যবহার করব। নিচে User এন্টিটি:

package com.example.demo;

import javax.persistence.Entity;
import javax.persistence.GeneratedValue;
import javax.persistence.GenerationType;
import javax.persistence.Id;

@Entity
public class User {
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;
    private String name;
    private String email;

    // getters and setters
    public Long getId() { return id; }
    public void setId(Long id) { this.id = id; }
    public String getName() { return name; }
    public void setName(String name) { this.name = name; }
    public String getEmail() { return email; }
    public void setEmail(String email) { this.email = email; }
}

এই ক্লাসটি ডেটাবেসে User টেবিল ম্যাপ করবে। @Entity অ্যানোটেশনটি JPA-কে জানায় যে এটি একটি ডেটাবেস এন্টিটি।

২. দুটি সার্ভিস লেয়ার তৈরি করা

আমরা দুটি সার্ভিস ক্লাস তৈরি করব: UserService এবং LogServiceUserService-এ createUser মেথড থাকবে এবং LogService-এ logActivity মেথড থাকবে। আমরা দেখব কিভাবে propagation তাদের আচরণ পরিবর্তন করে।

package com.example.demo;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Propagation;
import org.springframework.transaction.annotation.Transactional;

@Service
public class UserService {

    @Autowired
    private UserRepository userRepository;

    @Autowired
    private LogService logService;

    @Transactional(propagation = Propagation.REQUIRED)
    public void createUser(String name, String email) {
        User user = new User();
        user.setName(name);
        user.setEmail(email);
        userRepository.save(user);

        // লগ সার্ভিস কল করছে, যা একটি নতুন ট্রানজ্যাকশন তৈরি করবে
        logService.logActivity("User created: " + name);
    }
}

এখানে createUser মেথডটি @Transactional(propagation = Propagation.REQUIRED) দিয়ে চিহ্নিত। এর মানে, যদি কলকারীর কোনো ট্রানজ্যাকশন না থাকে, তাহলে এটি নিজে একটি নতুন ট্রানজ্যাকশন শুরু করবে। কিন্তু এটি যখন logService.logActivity() কল করবে, তখন সেটির propagation কী?

package com.example.demo;

import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Propagation;
import org.springframework.transaction.annotation.Transactional;

@Service
public class LogService {

    @Autowired
    private LogRepository logRepository;

    @Transactional(propagation = Propagation.REQUIRES_NEW)
    public void logActivity(String message) {
        Log log = new Log();
        log.setMessage(message);
        logRepository.save(log);
    }
}

LogService-এর logActivity মেথডটি REQUIRES_NEW propagation ব্যবহার করে। এর মানে, এটি সবসময় একটি নতুন এবং স্বাধীন ট্রানজ্যাকশন তৈরি করবে। এমনকি যদি UserService-এর ট্রানজ্যাকশন ব্যর্থ হয় এবং রোলব্যাক হয়, তবুও LogService-এর ট্রানজ্যাকশন সফলভাবে কমিট হবে।

৩. রিপোজিটরি এবং এন্টিটি তৈরি করা

এখন Log এন্টিটি এবং LogRepository তৈরি করুন:

package com.example.demo;

import javax.persistence.Entity;
import javax.persistence.GeneratedValue;
import javax.persistence.GenerationType;
import javax.persistence.Id;

@Entity
public class Log {
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;
    private String message;

    // getters and setters
    public Long getId() { return id; }
    public void setId(Long id) { this.id = id; }
    public String getMessage() { return message; }
    public void setMessage(String message) { this.message = message; }
}

LogRepository:

package com.example.demo;

import org.springframework.data.jpa.repository.JpaRepository;

public interface LogRepository extends JpaRepository<Log, Long> {
}

সম্পূর্ণ উদাহরণ (Complete Example)

এখন একটি সম্পূর্ণ Spring Boot অ্যাপ্লিকেশন লিখি যেখানে আমরা UserService এবং LogService ব্যবহার করব এবং একটি REST কন্ট্রোলার থেকে কল করব। আমরা দেখব কিভাবে REQUIRES_NEW propagation কাজ করে যখন UserService-এর ট্রানজ্যাকশন ব্যর্থ হয়।

package com.example.demo;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;

@RestController
public class TestController {

    @Autowired
    private UserService userService;

    @GetMapping("/test")
    public String testPropagation() {
        try {
            // একটি ইউজার তৈরি করার চেষ্টা করছি, কিন্তু ইচ্ছাকৃতভাবে ব্যর্থ করব
            userService.createUser("John", "john@example.com");
        } catch (Exception e) {
            // ব্যর্থতা সত্ত্বেও, লগটি সেভ হবে কিনা?
        }
        return "Check the database for logs and users";
    }
}

এখন UserService-এ আমরা ইচ্ছাকৃতভাবে একটি RuntimeException ছুড়বো:

@Transactional(propagation = Propagation.REQUIRED)
public void createUser(String name, String email) {
    User user = new User();
    user.setName(name);
    user.setEmail(email);
    userRepository.save(user);

    // এখানে ইচ্ছাকৃতভাবে exception ছুড়ছি
    if (name.equals("John")) {
        throw new RuntimeException("Simulated error - user creation failed");
    }

    logService.logActivity("User created: " + name);
}

এখন অ্যাপ্লিকেশন রান করলে এবং /test এন্ডপয়েন্টে কল করলে কী হবে? UserService-এর createUser মেথডটি REQUIRED propagation ব্যবহার করে, তাই এটি একটি নতুন ট্রানজ্যাকশন শুরু করে। কিন্তু যখন RuntimeException ছোড়া হয়, তখন পুরো createUser মেথডের ট্রানজ্যাকশন রোলব্যাক হয়ে যাবে। ফলে userRepository.save(user)-এর কাজও বাতিল হবে। কিন্তু logService.logActivity() কলটি কখনোই এক্সিকিউট হবে না, কারণ exception এর আগেই থ্রো হয়েছে। তাই LogService-এর REQUIRES_NEW propagation এখানে কোনো প্রভাব ফেলবে না।

আসল পরীক্ষা: LogService-এর logActivity মেথডটি যদি createUser মেথডের শুরুতে কল করা হয় এবং পরে exception আসে, তাহলে LogService-এর ট্রানজ্যাকশন আলাদাভাবে কমিট হবে।

@Transactional(propagation = Propagation.REQUIRED)
public void createUser(String name, String email) {
    // প্রথমে লগ করি
    logService.logActivity("Attempting to create user: " + name);

    User user = new User();
    user.setName(name);
    user.setEmail(email);
    userRepository.save(user);

    // এখন exception
    if (name.equals("John")) {
        throw new RuntimeException("Simulated error");
    }
}

এই ক্ষেত্রে, logService.logActivity() সফলভাবে কমিট হবে (কারণ এটি REQUIRES_NEW ব্যবহার করে), কিন্তু userRepository.save(user) রোলব্যাক হবে। ডেটাবেসে লগ টেবিলে একটি এন্ট্রি থাকবে, কিন্তু ইউজার টেবিলে কোনো এন্ট্রি থাকবে না।

সাধারণ সমস্যা ও সমাধান (Common Issues)

১. REQUIRES_NEW কাজ করছে না

সমস্যা: যদি আপনি দেখেন যে REQUIRES_NEW propagation সঠিকভাবে কাজ করছে না এবং ট্রানজ্যাকশন রোলব্যাক হচ্ছে, তাহলে কারণ হতে পারে আপনার @Transactional অ্যানোটেশনটি শুধুমাত্র পাবলিক মেথডে কাজ করে। Spring AOP-এর কারণে প্রাইভেট মেথডে @Transactional কাজ করে না।

সমাধান: নিশ্চিত করুন যে আপনার মেথডটি public এবং একই ক্লাসের মধ্যে অন্য @Transactional মেথডকে কল করছেন না (self-invocation)। কারণ self-invocation-এ Spring AOP কাজ করে না।

২. MANDATORY propagation ব্যবহার

MANDATORY propagation বলবে যে কলকারীর অবশ্যই একটি ট্রানজ্যাকশন থাকতে হবে। যদি না থাকে, তাহলে IllegalTransactionStateException থ্রো হবে। উদাহরণ:

@Transactional(propagation = Propagation.MANDATORY)
public void mandatoryMethod() {
    // ডেটাবেস অপারেশন
}

এই মেথডটি যদি কোনো ট্রানজ্যাকশন ছাড়া কল করা হয়, তাহলে exception আসবে।

৩. NESTED propagation

NESTED propagation JDBC সেভপয়েন্ট ব্যবহার করে। এটি শুধুমাত্র কিছু ডেটাবেস (যেমন Oracle) এবং কিছু ট্রানজ্যাকশন ম্যানেজারে (যেমন DataSourceTransactionManager) কাজ করে। JPA-তে এটি সাধারণত কাজ করে না।

সারসংক্ষেপ (Summary)

এই টিউটোরিয়ালে আমরা Spring-এর @Transactional অ্যানোটেশনের propagation অ্যাট্রিবিউট সম্পর্কে বিস্তারিত জানলাম। আমরা দেখলাম কিভাবে REQUIRED, REQUIRES_NEW, এবং MANDATORY propagation মোডগুলি কাজ করে এবং একটি সম্পূর্ণ উদাহরণের মাধ্যমে বুঝলাম যে, কিভাবে REQUIRES_NEW ব্যবহার করে ব্যর্থ ট্রানজ্যাকশনের পরেও লগিং অপারেশন সফল রাখা যায়।

মূল টেকঅ্যাওয়েস:

  • REQUIRED (ডিফল্ট) একই ট্রানজ্যাকশনে যোগ দেয়।
  • REQUIRES_NEW সবসময় একটি নতুন, স্বাধীন ট্রানজ্যাকশন তৈরি করে।
  • MANDATORY বাধ্যতামূলকভাবে কলকারীর ট্রানজ্যাকশন দাবি করে।
  • Self-invocation এবং প্রাইভেট মেথডে @Transactional কাজ করে না।

আপনার অ্যাপ্লিকেশনের লজিক অনুযায়ী সঠিক propagation মোড নির্বাচন করুন। এটি ডেটাবেসের ডেটা ইন্টিগ্রিটি বজায় রাখতে সাহায্য করবে।

Share