JPA — Best Way To Use @Transactional

Prateek
4 min readJun 29, 2021

This application is meant to highlight that the best place to use @Transactional for user defined query-methods is in repository interface, and afterwards, depending on situation, on service-methods level.

Author.java

@Data
@Entity
public class Author implements Serializable {

private static final long serialVersionUID = 1L;

@Id
private Long id;

private int age;
private String name;
private String genre;
}

AuthorRepository.java

@Repository
@Transactional(readOnly = true)
public interface AuthorRepository extends JpaRepository<Author, Long> {

@Query("SELECT a FROM Author a WHERE a.name = ?1")
public Author fetchByName(String name);

@Transactional
@Modifying
@Query("DELETE FROM Author a WHERE a.genre <> ?1")

public int deleteByNeGenre(String genre);
}

BookStoreService.java

@RequiredArgsConstructor
@Service
public class BookstoreService {

private final AuthorRepository authorRepository;

@Transactional
public void longRunningServiceMethod() throws InterruptedException {

System.out.println("Service-method start ...");
System.out.println("Sleeping before triggering SQL to simulate a long running code ...");
Thread.sleep(40000);

Author author = authorRepository.fetchByName("Joana Nimar");
authorRepository.deleteByNeGenre(author.getGenre());

System.out.println("Service-method done ...");
}

@Transactional
public void newAuthors() {

Author a1 = new Author();
a1.setName("Mark Janel");
a1.setGenre("Anthology");
a1.setId(1L);
a1.setAge(23);

Author a2 = new Author();
a2.setName("Olivia Goy");
a2.setGenre("Anthology");
a2.setId(2L);
a2.setAge(43);

Author a3 = new Author();
a3.setName("Quartis Young");
a3.setGenre("Anthology");
a3.setId(3L);
a3.setAge(51);

Author a4 = new Author();
a4.setName("Joana Nimar");
a4.setGenre("History");
a4.setId(4L);
a4.setAge(34);

authorRepository.saveAll(Arrays.asList(a1, a2, a3, a4));
}
}

application.properties

spring.datasource.url=jdbc:mysql://localhost:3306/bookstoredb?createDatabaseIfNotExist=true&useSSL=false
spring.datasource.username=root
spring.datasource.password=root

spring.jpa.hibernate.ddl-auto=create
spring.jpa.show-sql=true

spring.jpa.properties.hibernate.dialect=org.hibernate.dialect.MySQL8Dialect
spring.jpa.properties.hibernate.format-sql=true

spring.jpa.open-in-view=false

logging.level.ROOT=INFO
logging.level.org.hibernate.engine.transaction.internal.TransactionImpl=DEBUG
logging.level.org.springframework.orm.jpa=DEBUG
logging.level.org.springframework.transaction=DEBUG

logging.level.com.zaxxer.hikari.HikariConfig=DEBUG
logging.level.com.zaxxer.hikari=DEBUG

spring.datasource.hikari.auto-commit=false
spring.jpa.properties.hibernate.connection.provider_disables_autocommit=true

Console

Hibernate: insert into author (age, genre, name, id) values (?, ?, ?, ?)
Hibernate: insert into author (age, genre, name, id) values (?, ?, ?, ?)
Hibernate: insert into author (age, genre, name, id) values (?, ?, ?, ?)
Hibernate: insert into author (age, genre, name, id) values (?, ?, ?, ?)

2021-06-29 21:22:13.432 DEBUG 45424 --- [ main] o.s.orm.jpa.JpaTransactionManager : Closing JPA EntityManager [SessionImpl(1957005830<open>)] after transaction
=========================================
2021-06-29 21:22:13.432 DEBUG 45424 --- [ main] o.s.orm.jpa.JpaTransactionManager : Creating new transaction with name [com.bookstore.service.BookstoreService.longRunningServiceMethod]: PROPAGATION_REQUIRED,ISOLATION_DEFAULT
2021-06-29 21:22:13.432 DEBUG 45424 --- [ main] o.s.orm.jpa.JpaTransactionManager : Opened new EntityManager [SessionImpl(46727015<open>)] for JPA transaction
2021-06-29 21:22:13.433 DEBUG 45424 --- [ main] o.h.e.t.internal.TransactionImpl : On TransactionImpl creation, JpaCompliance#isJpaTransactionComplianceEnabled == false
2021-06-29 21:22:13.433 DEBUG 45424 --- [ main] o.h.e.t.internal.TransactionImpl : begin
2021-06-29 21:22:13.433 DEBUG 45424 --- [ main] o.s.orm.jpa.JpaTransactionManager : Exposing JPA transaction as JDBC [org.springframework.orm.jpa.vendor.HibernateJpaDialect$HibernateConnectionHandle@21733cbe]
Service-method start ...
Sleeping before triggering SQL to simulate a long running code ...
2021-06-29 21:22:41.313 DEBUG 45424 --- [l-1 housekeeper] com.zaxxer.hikari.pool.HikariPool : HikariPool-1 - Pool stats (total=10, active=0, idle=10, waiting=0)
2021-06-29 21:22:53.433 DEBUG 45424 --- [ main] o.s.orm.jpa.JpaTransactionManager : Found thread-bound EntityManager [SessionImpl(46727015<open>)] for JPA transaction
2021-06-29 21:22:53.433 DEBUG 45424 --- [ main] o.s.orm.jpa.JpaTransactionManager : Participating in existing transaction
Hibernate: select author0_.id as id1_0_, author0_.age as age2_0_, author0_.genre as genre3_0_, author0_.name as name4_0_ from author author0_ where author0_.name=?
2021-06-29 21:22:53.479 DEBUG 45424 --- [ main] o.s.orm.jpa.JpaTransactionManager : Found thread-bound EntityManager [SessionImpl(46727015<open>)] for JPA transaction
2021-06-29 21:22:53.480 DEBUG 45424 --- [ main] o.s.orm.jpa.JpaTransactionManager : Participating in existing transaction
Hibernate: delete from author where genre<>?
Service-method done ...
2021-06-29 21:22:53.490 DEBUG 45424 --- [ main] o.s.orm.jpa.JpaTransactionManager : Initiating transaction commit
2021-06-29 21:22:53.490 DEBUG 45424 --- [ main] o.s.orm.jpa.JpaTransactionManager : Committing JPA transaction on EntityManager [SessionImpl(46727015<open>)]
2021-06-29 21:22:53.490 DEBUG 45424 --- [ main] o.h.e.t.internal.TransactionImpl : committing
2021-06-29 21:22:53.520 DEBUG 45424 --- [ main] o.s.orm.jpa.JpaTransactionManager : Closing JPA EntityManager [SessionImpl(46727015<open>)] after transaction
=========================================

— — — — — — — -

Why the @Transactional annotation is being ignored

Description: This application is an example of fixing the case when @Transactional annotation is ignored. Most of the time, this annotation is ignored in the following scenarios:

  1. @Transactional was added to a private, protected or package-protected method
  2. @Transactional was added to a method defined in the same class where it is invoked

Key points:

  • write a helper service and move the @Transactional methods there
  • ensure that these methods are declared as public
  • call @Transactional methods from other services

Author.java

@Data
@Entity
public class Author implements Serializable {

private static final long serialVersionUID = 1L;

@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;

private int age;
private String name;
private String genre;
}

AuthorRepository.java

public interface AuthorRepository extends JpaRepository<Author, Long> {
}

HelperService.java

@RequiredArgsConstructor
@Service
public class HelperService {

private final AuthorRepository authorRepository;

@Transactional(propagation = Propagation.REQUIRES_NEW)
public long persistAuthor(Author author) {
authorRepository.save(author);
return authorRepository.count();
}
}

BookStoreService.java

@Service
@RequiredArgsConstructor
public class BookstoreService {

private static final Logger log = Logger.getLogger(BookstoreService.class.getName());

private final AuthorRepository authorRepository;
private final HelperService helperService;

public void mainAuthor() {
Author author = new Author();
// persistAuthor(author);
helperService.persistAuthor(author);
notifyAuthor(author);
}

@Transactional(propagation = Propagation.REQUIRES_NEW)
protected long persistAuthor(Author author) {
authorRepository.save(author);
return authorRepository.count();
}

private void notifyAuthor(Author author) {
log.info(() -> "Saving author: " + author);
}
}

MainApp.java

@RequiredArgsConstructor
@SpringBootApplication
public class MainApplication {

private final BookstoreService bookstoreService;

public static void main(String[] args) {
SpringApplication.run(MainApplication.class, args);
}

@Bean
public ApplicationRunner init() {
return args -> {
bookstoreService.mainAuthor();
};
}
}

application.properties

spring.datasource.url=jdbc:mysql://localhost:3306/bookstoredb?createDatabaseIfNotExist=true
spring.datasource.username=root
spring.datasource.password=root

spring.jpa.hibernate.ddl-auto=create
spring.jpa.show-sql=true

spring.jpa.properties.hibernate.dialect=org.hibernate.dialect.MySQL5Dialect

spring.jpa.open-in-view=false

logging.level.ROOT=INFO
logging.level.org.springframework.orm.jpa=DEBUG
logging.level.org.springframework.transaction=DEBUG

Console

2021-06-29 21:59:05.459 DEBUG 25652 --- [           main] o.s.orm.jpa.JpaTransactionManager        : Participating in existing transaction
Hibernate: insert into author (age, genre, name) values (?, ?, ?)
2021-06-29 21:59:05.506 DEBUG 25652 --- [ main] o.s.orm.jpa.JpaTransactionManager : Found thread-bound EntityManager [SessionImpl(817092761<open>)] for JPA transaction
2021-06-29 21:59:05.506 DEBUG 25652 --- [ main] o.s.orm.jpa.JpaTransactionManager : Participating in existing transaction
2021-06-29 21:59:05.520 INFO 25652 --- [ main] o.h.h.i.QueryTranslatorFactoryInitiator : HHH000397: Using ASTQueryTranslatorFactory
Hibernate: select count(*) as col_0_0_ from author author0_
2021-06-29 21:59:05.574 DEBUG 25652 --- [ main] o.s.orm.jpa.JpaTransactionManager : Initiating transaction commit
2021-06-29 21:59:05.575 DEBUG 25652 --- [ main] o.s.orm.jpa.JpaTransactionManager : Committing JPA transaction on EntityManager [SessionImpl(817092761<open>)]
2021-06-29 21:59:05.577 DEBUG 25652 --- [ main] o.s.orm.jpa.JpaTransactionManager : Closing JPA EntityManager [SessionImpl(817092761<open>)] after transaction
2021-06-29 21:59:05.577 INFO 25652 --- [ main] com.bookstore.service.BookstoreService : Saving author: Author(id=1, age=0, name=null, genre=null)

--

--