When using @Transactional
on method of a bean or on the bean class, spring
will create a Proxy for the bean.
Given following service:
@Service
public class UserServiceImpl implements UserService {
private UserRepository userRepository;
public UserServiceImpl(UserRepository userRepository) {
this.userRepository = userRepository;
}
@Transactional
@Override
public void insert(UserEntity userEntity) {
userRepository.save(userEntity);
}
}
The generated proxy looks "like" this:
public class UserServiceImpl$$SpringCGLIB$$0 extends UserServiceImpl implements SpringProxy, Advised, Factory {
// ...
public void insert(UserEntity userEntity) {
MethodInterceptor interceptor = this.CGLIB$CALLBACK_0;
if (interceptor == null) {
CGLIB$BIND_CALLBACKS(this);
interceptor = this.CGLIB$CALLBACK_0;
}
if (interceptor != null) {
interceptor.intercept(this, CGLIB$insert$0$Method, new Object[]{var1}, CGLIB$insert$0$Proxy);
} else {
super.insert(var1);
}
}
// ...
}
In context of transaction management, in code above, interceptor
is a TransactionInterceptor
Which performs:
public Object invoke(MethodInvocation invocation) throws Throwable {
Class<?> targetClass = (invocation.getThis() != null ? AopUtils.getTargetClass(invocation.getThis()) : null);
return invokeWithinTransaction(invocation.getMethod(), targetClass, invocation::proceed);
}
protected Object invokeWithinTransaction(Method method, @Nullable Class<?> targetClass, final InvocationCallback invocation) throws Throwable {
//.....
// nmeylan: platformTransactionManager is JpaTransactionManager when using spring-orm with default config
// nmeylan: platformTransactionManager is DataSourceTransactionManager when using a jdbc datasource
TransactionInfo txInfo = createTransactionIfNecessary(platformTransactionManager, txAttr, joinpointIdentification);
Object retVal;
try {
// This is an around advice: Invoke the next interceptor in the chain.
// This will normally result in a target object being invoked.
retVal = invocation.proceedWithInvocation();
}
catch (Throwable ex) {
// target invocation exception
completeTransactionAfterThrowing(txInfo, ex);
throw ex;
}
finally {
cleanupTransactionInfo(txInfo);
}
//.....
Without the proxy we would manually have written this, that is what the proxy does:
public void insert(UserEntity userEntity) {
Connection connection = dataSource.getConnection();
try (connection) {
connection.setAutoCommit(false);
userRepository.save(userEntity);
connection.commit();
} catch (SQLException e) {
connection.rollback();
}
}
If we execute the code with a breakpoint in datasource.getConnection()
implementation, here the stacktrace we get:
It opens or gets a connection from the connection pool at this moment.
Note that transaction is not opened yet on database server, it comes later with the first query being executed.
With the knowledge of what using @Transactional
implies, let see some common mistakes.
Almost all resources on internet highlight that @Transactional
should be used at Service
and it should not be done at Repository
layer because it is the responsibility of the service to orchestrate database call and knows when to open transaction.
Those articles are not wrong because they only takes examples of simple CRUD application where services are pass-through to the database.
The issue with this recommendation is it is not a one-size-fit-all solution, and it should be applied carefully.
The code of below examples for this example can be found on github.
Very often the code wrapped in the transaction is not doing only interaction with database
public class UserServiceImpl implements UserService {
private UserRepository userRepository;
private EmailSender emailSender;
public UserServiceImpl(UserRepository userRepository) {
this.userRepository = userRepository;
}
@Transactional
@Override
public void registerUser(UserEntity userEntity) {
userRepository.save(userEntity);
// Just for the sake of example, in real life we prefer to send email to the user once the transaction is commited
// to not send email if the transaction rollback or fail to commit due to network issue
emailSender.send(userEntity.getEmail(), "Welcome " + userEntity.getName())");
}
}
In this case it would have been better to keep transaction management at repository
layer by not putting @Transactional
on this method, this is handle by spring-data-jpa.
What happens if SMTP server is slow or does not responds with a timeout at 1 minute? The transaction stay open for the whole duration of the registerUser
execution.
What are the consequences of a transactions being open when using postgresql:
VACUUM
or auto vacuum
.To summarize it bring stability and performance degradation.
As stated before, many developers are using @Transactional
at class level on their services, but it often leads to mistakes.
Sometimes it is even worse, this is done on the interface
without knowing what the actual implementation will do (which is the purpose of an interface no?).
Both approach can be justified if you know what you do, one may use @Transactional(propagation = Propagation.NEVER)
when transaction are not needed, but you don't know how code will be added** to this class later.
A typical example:
@Service
@Transactional // <- Notice @Transactional at class level
public class UserServiceImpl implements UserService {
private UserRepository userRepository;
public UserServiceImpl(UserRepository userRepository) {
this.userRepository = userRepository;
}
@Override
public void insert(UserEntity userEntity) {
userRepository.save(userEntity);
}
@Override
public void nameValidation(String name) {
// Simulating calling a slow external service to check if name is valid
try {
sleep(1000 * 60);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
}
}
Now when we call nameValidation
method, it gets a connection, even if we don't interact with the database.
What are the consequences of getting a connection:
This test shows illustrate it:
public class UserServiceIT extends IntegrationTestClass{
@Autowired
UserService userService;
@Autowired
HikariDataSource dataSource;
@Test
public void nameValidation() throws InterruptedException {
// Given
ExecutorService executorService = Executors.newFixedThreadPool(12);
// When
for(int i = 0; i < 12; i++) {
CompletableFuture.runAsync(() -> userService.nameValidation("John doe"), executorService);
}
// Waiting few ms to get the MXBean stats refreshed
Thread.sleep(100);
// Then
// max pool size is 10
assertThat(dataSource.getHikariPoolMXBean().getActiveConnections()).isEqualTo(10);
assertThat(dataSource.getHikariPoolMXBean().getThreadsAwaitingConnection()).isEqualTo(2);
}
}
When all connections of the pool are active, code requiring a connection will just wait until it gets a connection or timeout.
Not good for the whole application or product stability and performance, just because @Transactional
was wrongly used.
This one is an often visible and well documented mistake.
@Service
public class UserServiceImpl implements UserService {
private UserRepository userRepository;
private UserStatsRepository userStatsRepository;
public UserServiceImpl(UserRepository userRepository, UserStatsRepository userStatsRepository) {
this.userRepository = userRepository;
this.userStatsRepository = userStatsRepository;
}
@Transactional
@Override
public void insert(UserEntity userEntity) {
userRepository.save(userEntity);
this.forceIncrementUserInsertStats()
}
@Transactional(propagation = Propagation.REQUIRES_NEW)
@Override
private/public void forceIncrementUserInsertStats() {
userStatsRepository.incNewUserCount();
}
}
In both case no new transaction will be created when calling forceIncrementUserInsertStats
:
public
calling internal method call the actual method not the proxy one.private
there is no proxy generated at all, but also same as for public calling internal method call the actual method not the proxy one.Let's finish on a correct usage of @Transactional
.
If we take our example above where we wanted to persist
a user and send him an email
after he is registered in our system.
Requirements are strict:
For this lets say we are using a library (like jobrunr) which allow to register job in database, and job will be guaranteed to succeed.
We want to perform both insert
within the same transaction.
@Service
public class RegisterUserImpl {
UserPersistence userPersistence;
// This implementation of EmailSender does not actually send email, but register a durable job in database which will send email
DurableEmailSenderImpl durableEmailSender;
public RegisterUserImpl(UserPersistence userPersistence, DurableEmailSenderImpl durableEmailSender) {
this.userPersistence = userPersistence;
this.durableEmailSender = durableEmailSender;
}
@Transactional
public void register(UserEntity userEntity) {
userPersistence.save(userEntity);
durableEmailSender.sendEmail(userEntity.getEmail(), "Welcome " + userEntity.getFirstName());
}
}
@Transactional
is very convenient if you know what it implies!@Transactional
carefully, on purpose or "manually manage" transaction via TransactionTemplate to avoid easy mistakes.I encourage you to read this complete guide about spring transaction management by Marco Behler.