Spring Transaction Management
Spring Transaction Management
@Transactional In-Depth
Last updated on June 03, 2022 - 109 comments
Quick Links
Introduction
How plain JDBC Transaction Management works
How Spring’s or Spring Boot’s Transaction Management
works
How Spring and JPA / Hibernate Transaction
Management works
Fin
Acknowledgements
↑ Top
You can use this guide to get a simple and practical
understanding of how Spring's transaction management with
the @Transactional annotation works.
Introduction
In this guide you are going to learn about the main pillars
of Spring core’s transaction abstraction framework (a
confusing term, isn’t it?) - described with a lot of code
examples:
Why?
In the end, they all do the very same thing to open and close
(let’s call that 'manage') database transactions. Plain JDBC
transaction management code looks like this:
import java.sql.Connection;
try (connection) {
connection.setAutoCommit(false); // (2)
// execute some SQL statements...
connection.commit(); // (3)
} catch (SQLException e) {
connection.rollback(); // (4)
}
1. You need a connection to the database to start
transactions. DriverManager.getConnection(url, user,
password) would work as well, though in most
enterprise-y applications you will have a data source
configured and get connections from that.
2. This is the only way to 'start' a database transaction in
Java, even though the name might sound a bit
off. setAutoCommit(true) makes sure that every single
SQL statement automatically gets wrapped in its own
transaction and setAutoCommit(false) is the opposite:
You are the master of the transaction(s) and you’ll need
to start calling commit and friends. Do note,
the autoCommit flag is valid for the whole time your
connection is open, which means you only need to call
the method once, not repeatedly.
3. Let’s commit our transaction…
4. Or, rollback our changes, if there was an exception.
// isolation=TransactionDefinition.ISOLATION_READ_UNCOMMITTED
connection.setTransactionIsolation(Connection.TRANSACTION_READ_UNCOMMITTED); //
(1)
// propagation=TransactionDefinition.NESTED
Here’s the catch: Whereas with plain JDBC you only have one
way (setAutocommit(false)) to manage transactions, Spring
offers you many different, more convenient ways to achieve
the same.
@Autowired
private TransactionTemplate template;
Back in the day, when XML configuration was the norm for
Spring projects, you could configure transactions directly in
XML. Apart from a couple of legacy, enterprise projects, you
won’t find this approach anymore in the wild, as it has been
superseded with the much simpler @Transactional
annotation.
@Transactional
public Long registerUser(User user) {
// execute some SQL that e.g.
// inserts the user into the db and retrieves the autogenerated id
// userDao.save(user);
return id;
}
}
How is this possible? There is no more XML configuration and
there’s also no other code needed. Instead, you now need to
do two things:
@Bean
public PlatformTransactionManager txManager() {
return yourTxManager; // more on that later
}
}
Now, when I say Spring transparently handles transactions
for you. What does that really mean?
connection.commit(); // (1)
} catch (SQLException e) {
connection.rollback(); // (1)
}
}
}
1. This is all just standard opening and closing of a JDBC
connection. That’s what Spring’s transactional
annotation does for you automatically, without you
having to write it explicitly.
2. This is your own code, saving the user through a DAO or
something similar.
This example might look a bit magical, but let’s have a look
at how Spring inserts this connection code for you.
Spring cannot really rewrite your Java class, like I did above,
to insert the connection code (unless you are using advanced
techniques like bytecode weaving, but we are ignoring that
for now).
Quick Exam
@Bean
public UserService userService() { // (1)
return new UserService();
}
}
1. Correct. Spring constructs a dynamic CGLib proxy of
your UserService class here that can open and close
database transactions for you. You or any other beans
won’t even notice that it is not your UserService, but a
proxy wrapping your UserService.
@Bean
public PlatformTransactionManager txManager() {
return new DataSourceTransactionManager(dataSource()); // (2)
}
1. You create a database-specific or connection-pool
specific datasource here. MySQL is being used for this
example.
2. Here, you create your transaction manager, which needs
a data source to be able to manage transactions.
@Override
protected void doCommit(DefaultTransactionStatus status) {
// ...
Connection connection =
status.getTransaction().getConnectionHolder().getConnection();
try {
con.commit();
} catch (SQLException ex) {
throw new TransactionSystemException("Could not commit JDBC transaction",
ex);
}
}
}
So, the datasource transaction manager uses exactly the
same code that you saw in the JDBC section, when managing
transactions.
@Transactional
public void invoice() {
invoiceService.createPdf();
// send invoice as email, etc.
}
}
@Service
public class InvoiceService {
@Transactional
public void createPdf() {
// ...
}
}
UserService has a transactional invoice() method. Which calls
another transactional method, createPdf() on the
InvoiceService.
@Transactional(propagation = Propagation.REQUIRES_NEW)
public void createPdf() {
// ...
}
}
Changing the propagation mode to requires_new is telling
Spring that createPDF() needs to execute in its own
transaction, independent of any other, already existing
transaction. Thinking back to the plain Java section of this
guide, did you see a way to "split" a transaction in half?
Neither did I.
// or
@Transactional(propagation = Propagation.REQUIRES_NEW)
// etc
The full list:
REQUIRED
SUPPORTS
MANDATORY
REQUIRES_NEW
NOT_SUPPORTED
NEVER
NESTED
Exercise:
In the plain Java section, I showed you everything that JDBC
can do when it comes to transactions. Take a minute to think
about what every single Spring propagation mode at the
end REALLY does to your datasource or rather, your JDBC
connection.
Answers:
@Transactional(propagation = Propagation.MANDATORY)
public void myMethod() {
// execute some sql
}
}
In this case, Spring will expect a transaction to be open,
whenever you call myMethod() of the UserService class.
It does not open one itself, instead, if you call that method
without a pre-existing transaction, Spring will throw an
exception. Keep this in mind as additional points for "logical
transaction handling".
@Transactional
public void invoice() {
createPdf();
// send invoice as email, etc.
}
@Transactional(propagation = Propagation.REQUIRES_NEW)
public void createPdf() {
// ...
}
}
You have a UserService class with a transactional invoice
method. Which calls createPDF(), which is also transactional.
How many physical transactions would you expect to be
open, once someone calls invoice()?
@Autowired
private SessionFactory sessionFactory; // (1)
// and commit it
session.getTransaction().commit();
In plain code:
@Service
public class UserService {
@Autowired
private SessionFactory sessionFactory; // (1)
@Transactional
public void registerUser(User user) {
sessionFactory.getCurrentSession().save(user); // (2)
}
}
1. The same SessionFactory as before
2. But no more manual state management. Instead,
getCurrentSession() and @Transactional are in sync.