@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.