@Transactional annotation in Spring Boot

@Transactional annotation in Spring Boot

@Transactional annotation is used to mark a method or a class as transactional.

Any database operations performed within the marked method or class will be executed within a transaction.

If the transaction is successful, the changes will be committed to the database. If an error occurs and the transaction is rolled back, the changes will not be persisted in the database.

@Transactional annotation can be used on a method:

@Transactional
public void updateUser(User user) {
}

@Transactional annotation can be used on a class:

@Transactional
public class UserService {
  public void updateUser(User user) {
    // Perform database update here
  }
}

You can also specify the propagation behaviour of a transaction by using the propagation attribute of the @Transactional annotation. For example, you can specify that a transaction should always be created, regardless of whether a transaction already exists, by using the PROPAGATION_REQUIRED

@Transactional(propagation = Propagation.REQUIRED)
public void updateUser(User user) {
  // Perform database update here
}

You can also specify the isolation level of a transaction by using the isolation attribute. The isolation level determines how much a transaction is isolated from other transactions. For example, you can use the ISOLATION_SERIALIZABLE attribute to specify that the transaction should be completely isolated from other transactions:

 @Transactional(isolation = Isolation.SERIALIZABLE)
public void updateUser(User user) 
{ 
  // Perform database update here
}

You can also specify the maximum time that a transaction should take to complete by using the timeout attribute.

@Transactional(timeout = 10)
public void updateUser(User user) {
  // Perform database update here
}

Example:

Entity Class - Employee & Address

@Getter
@Setter
@NoArgsConstructor
@AllArgsConstructor
@ToString
@Entity
@Table(name="EMP_INFO")
public class Employee {

    @Id
    @GeneratedValue(strategy = GenerationType.AUTO)
    private int id;
    private String name;

}
@Getter
@Setter
@NoArgsConstructor
@AllArgsConstructor
@ToString
@Entity
@Table(name="ADD_INFO")
public class Address {

    @Id
    @GeneratedValue(strategy = GenerationType.AUTO)
    private Long id;
    private String address;

    // one to one mapping means,
    // one employee stays at one address only
    @OneToOne
    private Employee employee;

}

Repository - EmployeeRepository & AddressRepository

public interface EmployeeRepository extends JpaRepository<Employee, Integer> {
}
public interface AddressRepository extends JpaRepository<Address, Integer> { 
}

Service

@Service
public class EmployeeService {

    @Autowired
    private EmployeeRepository employeeRepository;

    @Autowired
    private AddressService addressService;

    @Transactional
    public Employee addEmployee(Employee employee) throws Exception {
        Employee employeeSavedToDB = this.employeeRepository.save(employee);

        Address address = new Address();
        address.setId(123L);
        address.setAddress("Kolkata");
        address.setEmployee(employee);

        // calling addAddress() method 
        // of AddressService class
        this.addressService.addAddress(address);
        return employeeSavedToDB;
    }
}
@Service
public class AddressService {

    @Autowired
    private AddressRepository addressRepository;

    public Address addAddress(Address address) {
        Address addressSavedToDB = this.addressRepository.save(address);
        return addressSavedToDB;
    }

}

Controller

@RestController
@RequestMapping("/api/employee")
public class Controller {

    @Autowired
    private EmployeeService employeeService;

    @PostMapping("/add")
    public ResponseEntity<Employee> saveEmployee(@RequestBody Employee employee) throws Exception{
        Employee employeeSavedToDB = this.employeeService.addEmployee(employee);
        return new ResponseEntity<Employee>(employeeSavedToDB, HttpStatus.CREATED);
    }
}

Up till now everything is file and when we hit our /add endpoint it will first go to EmployeeService and create an employee. Then it will create an Address using AddressService and Both data will store in DB.

Interrupt The Transaction

We will break down our transaction. For this, we will initialize the address object with a NULL value in our EmployeeService class.

@Service
public class EmployeeService {

    @Autowired
    private EmployeeRepository employeeRepository;

    @Autowired
    private AddressService addressService;

    @Transactional
    public Employee addEmployee(Employee employee) throws Exception {
        Employee employeeSavedToDB = this.employeeRepository.save(employee);

        // we will initialize the
        // address object as null
        Address address = null;
        address.setId(123L);
        address.setAddress("Varanasi");
        address.setEmployee(employee);

        // calling addAddress() method
        // of AddressService class
        this.addressService.addAddress(address);
        return employeeSavedToDB;
    }
}

We have initialized the address object as null and requested the application, we have an employee created in the database but the address information is not, as we have received a null pointer exception. But, this is not good practice in transaction management, as employees should be saved only when the address is saved and vice-versa.

Transaction Management

To overcome this problem, we will use @Transactional annotation. This will ensure that the transaction should be complete. That is, either both employee and address data should be stored or nothing will get stored.

We need to use @EnableTransactionManagement in the main class of our spring boot application (Not strictly mandatory, but highly recommended if you need to manage database operations transactionally) and also, and we need to annotate our addEmployee() method in EmployeeService class with @Transactional annotation.