In Spring, @Transacional
annotation is used for indicating a method run inside a database transaction. It can also be annotated on the class level which applies as a default to all methods of the declaring class and its subclasses
Say you have a crudAgainstDatabase
method annotated with @Transacional
as below
@Transactional
public void crudAgainstDatabase() {
readFromDatabase();
writeToDatabase();
}
Spring AOP will create a proxy class and wraps the annotated method as the following pseudocode
Transaction tx = entityManager.getTransaction();
public void crudAgainstDatabaseWrapper() {
try {
tx.createANewTransactionIfNecessary();
crudAgainstDatabase();
tx.commitTransaction();
} catch(RuntimeException e) {
tx.rollbackTransaction();
}
}
The dependencies
To use @Transactional
, include Spring Data JPA dependency into your project. In Spring Boot, you can include spring-boot-starter-data-jpa
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-jpa</artifactId>
</dependency>
@Transactional isolation
You can declare which isolation level you'd like to use with this property. The options are DEFAULT
, READ_UNCOMMITTED
, READ_COMMITTED
, REPEATABLE_READ
, SERIALIZABLE
Learn more about Transaction isolation levels
The default and most practical option is Isolation.DEFAULT
which delegates the setting to the underlying database
This property only applies to the newly started transactions so it only works with Propagation.REQUIRED
and Propagation.REQUIRED_NEW
The below example set the isolation level to READ_COMMITTED
@Transactional(isolation = Isolation.READ_COMMITTED)
public void crudAgainstDatabase() {
...
}
@Transactional propagation
You can control either to use a current transaction to execute CRUD operations against a database or suspend it with the following options
Use a current transaction, if none exists then create a new one with
Propagation.REQUIRED
, execute non-transactionally withPropagation.SUPPORTS
, and throw an exception withPropagation.MANDATORY
Suspend a current transaction, if none exists then create a new transaction with
Propagation.REQUIRED_NEW
, execute non-transactionally withPropagation.NOT_SUPPORTED
The default and most practical option is Propagation.REQUIRED
The below example set the isolation level to READ_COMMITTED
and propagation to REQUIRED
@Transactional(
isolation = Isolation.READ_COMMITTED,
propagation = Propagation.REQUIRED)
public void crudAgainstDatabase() {
...
}
@Transactional rollbackFor
and noRollbackFor
- You can use
rollbackFor
to indicate which exception types must cause a transaction rollback. By default, they are unchecked exceptions includingRuntimeException
,Error
and their subclasses
Say you throw a RuntimeException
in your custom Transactional
method
@Transactional
public void updateWithThrowingRuntimeException(Long id, String name) {
Product product = findById(id).get();
product.setName(name);
throw new MyRuntimeException();
}
static class MyRuntimeException extends RuntimeException {
}
Then the transaction is rollbacked by the throwing MyRuntimeException
as expected
@Test
public void testRollbackRuntimeException() {
try {
productService.updateWithThrowingRuntimeException(1L, "updated");
} catch (RuntimeException e) {
System.out.println(e.getMessage());
}
Optional<Product> updatedProduct = productService.findByName("updated");
assertThat(updatedProduct).isNotPresent();
}
- Checked exceptions,
Exception
and its subclasses, don't cause transaction rollback by default
Say you throw an Exception
in your custom Transactional
method
@Transactional
public void updateWithThrowingException(Long id, String name) throws Exception {
Product product = findById(id).get();
product.setName(name);
throw new MyException();
}
static class MyException extends Exception {
}
Then the transaction is not rollbacked by the throwing MyException
as expected
@Test
public void testRollbackException() {
try {
productService.updateWithThrowingException(1L, "updated");
} catch (Exception e) {
System.out.println(e.getMessage());
}
Optional<Product> updatedProduct = productService.findByName("updated");
assertThat(updatedProduct).isPresent();
}
- Use
noRollbackFor
to indicate which exception types must not cause a transaction rollback
Say you throw a MyException
in your custom @Transactional(noRollbackFor = MyException.class)
method
@Transactional(noRollbackFor = MyException.class)
public void updateWithNoRollbackFor(Long id, String name) throws Exception {
Product product = findById(id).get();
product.setName(name);
throw new MyException();
}
Then the transaction is not rollbacked by the throwing MyException
as expected
@Test
public void testNoRollbackFor() {
try {
productService.updateWithNoRollbackFor(1L, "updated");
} catch (Exception e) {
System.out.println(e.getMessage());
}
Optional<Product> updatedProduct = productService.findByName("updated");
assertThat(updatedProduct).isPresent();
}
@Transactional readOnly
A boolean flag served as a hint for the actual transaction subsystems
The default value is false
. In Spring Data JPA and MySQL, an exception will be thrown when persist or delete operations against a database is called inside a method annotated with @Transactional(readOnly=true)
java.sql.SQLException: Connection is read-only.
Queries leading to data modification are not allowed
readOnly=true
can trigger a few performance optimizations in the underlying entity manager such as skipping dirty checks when closing the EntityManager via setting the FlushMode to MANUAL
@Transactional pitfalls
- You don't have to call save/update method against database explicitly inside a @Transactional method
Say you have a Transactional
method in ProductService
class
@Transactional
public void updateImplicitly(Long id, String name) {
Product product = findById(id).get();
product.setName(name);
// productRespository.save(product);
}
The update is still successfully hit the database
@Test
public void testUpdateImplicitly() {
productService.updateImplicitly(1L, "updated");
Optional<Product> updatedProduct = productService.findByName("updated");
assertThat(updatedProduct).isPresent();
}
There are no redundant JPA calls to the database. However, you may encounter an issue like the following example which might not work as expected
@Transactional
public void updateOnCondition(Long id, String name) {
Product product = findById(id).get();
product.setName(name);
if (product.getPrice().compareTo(new BigDecimal("10")) == 0) {
productRespository.save(product);
}
}
- As the limitation of Spring AOP, @Transactional can not work with non-public methods and in the same class method call (self-invocation)
Say you have a non-public Transactional
method defined in a Service
class
@Transactional
void updateImplicitlyNonPublic(Long id, String name) {
Product product = findById(id).get();
product.setName(name);
}
Then the update does not hit the database as expected
@Test
public void testUpdateImplicitlyNonPublic() {
productService.updateImplicitlyNonPublic(1L, "updated");
Optional<Product> updatedProduct = productService.findByName("updated");
assertThat(updatedProduct).isNotPresent();
}
Conclusion
In this tutorial, we learned how to use @Transactional
in Spring Data JPA through various practical examples. You can find the full source code as below
[TransactionalProductService.java]
import lombok.RequiredArgsConstructor;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import java.math.BigDecimal;
import java.util.Optional;
@RequiredArgsConstructor
@Service
public class TransactionalProductService {
private final ProductRespository productRespository;
public Optional<Product> findById(Long id) {
return productRespository.findById(id);
}
public Optional<Product> findByName(String name) {
return productRespository.findByName(name);
}
public Product save(Product stock) {
return productRespository.save(stock);
}
@Transactional
public void updateImplicitly(Long id, String name) {
Product product = findById(id).get();
product.setName(name);
// productRespository.save(product);
}
@Transactional
public void updateOnCondition(Long id, String name) {
Product product = findById(id).get();
product.setName(name);
if (product.getPrice().compareTo(new BigDecimal("10")) == 0) {
productRespository.save(product);
}
}
@Transactional
void updateImplicitlyNonPublic(Long id, String name) {
Product product = findById(id).get();
product.setName(name);
}
@Transactional
public void updateWithThrowingRuntimeException(Long id, String name) {
Product product = findById(id).get();
product.setName(name);
throw new MyRuntimeException();
}
static class MyRuntimeException extends RuntimeException {
}
@Transactional
public void updateWithThrowingException(Long id, String name) throws Exception {
Product product = findById(id).get();
product.setName(name);
throw new MyException();
}
static class MyException extends Exception {
}
@Transactional(noRollbackFor = MyException.class)
public void updateWithNoRollbackFor(Long id, String name) throws Exception {
Product product = findById(id).get();
product.setName(name);
throw new MyException();
}
}
[TransactionalTest.java]
import org.junit.Before;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.test.context.junit4.SpringRunner;
import java.math.BigDecimal;
import java.util.Optional;
import static org.assertj.core.api.Assertions.assertThat;
@RunWith(SpringRunner.class)
@SpringBootTest
public class TransactionalTest {
@Autowired
private TransactionalProductService productService;
@Before
public void setUp(){
// given
Product product = Product.builder()
.name("P1")
.description("P1 desc")
.price(new BigDecimal("1"))
.build();
productService.save(product);
}
@Test
public void testUpdateImplicitly() {
productService.updateImplicitly(1L, "updated");
Optional<Product> updatedProduct = productService.findByName("updated");
assertThat(updatedProduct).isPresent();
}
@Test
public void testUpdateOnCondition() {
productService.updateOnCondition(1L, "updated");
Optional<Product> updatedProduct = productService.findByName("updated");
assertThat(updatedProduct).isPresent();
}
@Test
public void testUpdateImplicitlyNonPublic() {
productService.updateImplicitlyNonPublic(1L, "updated");
Optional<Product> updatedProduct = productService.findByName("updated");
assertThat(updatedProduct).isNotPresent();
}
@Test
public void testRollbackRuntimeException() {
try {
productService.updateWithThrowingRuntimeException(1L, "updated");
} catch (RuntimeException e) {
System.out.println(e.getMessage());
}
Optional<Product> updatedProduct = productService.findByName("updated");
assertThat(updatedProduct).isNotPresent();
}
@Test
public void testRollbackException() {
try {
productService.updateWithThrowingException(1L, "updated");
} catch (Exception e) {
System.out.println(e.getMessage());
}
Optional<Product> updatedProduct = productService.findByName("updated");
assertThat(updatedProduct).isPresent();
}
@Test
public void testNoRollbackFor() {
try {
productService.updateWithNoRollbackFor(1L, "updated");
} catch (Exception e) {
System.out.println(e.getMessage());
}
Optional<Product> updatedProduct = productService.findByName("updated");
assertThat(updatedProduct).isPresent();
}
}