@Transactional
Introduction
Thanks to the excellent combination between AOP
and Google Guice, users can drastically reduce the boilerplate
code into their DAOs.
Let's take in consideration the following code snippet, written without introducing mybatis-guice:
package com.acme;
import org.apache.ibatis.session.*;
import org.mybatis.guice.transactional.*;
public final class FooDAO {
private final SqlSessionManager sessionManager;
public FooDAO(SqlSessionManager sessionManager) {
this.sessionManager = sessionManager;
}
public void doFooBar() throws MyDaoException {
// Starts a new SqlSession
this.sessionManager.startManagedSession(ExecutorType.BATCH,
TransactionIsolationLevel.READ_UNCOMMITTED);
try {
// Retrieve the FooMapper and execute the doFoo() method.
FooMapper fooMapper = this.sessionManager.getMapper(FooMapper.class);
fooMapper.doFoo();
// Retrieve the BarMapper and execute the doBar() method.
BarMapper barMapper = this.sessionManager.getMapper(BarMapper.class);
barMapper.doBar();
// If everything gone fine, commit the open session.
this.sessionManager.commit();
} catch (Throwable t) {
// If something gone wrong, rollback the open session.
this.sessionManager.rollback();
// Optionally, throw a proper DAO layer Exception
throw new MyDaoException("Something went wrong", t);
} finally {
// Close the session.
this.sessionManager.close();
}
}
}
Users can easily note that this is a recursive and redundant code
pattern that mybatis-guice will help to simplify introducing a
special AOP
interceptor.
The @Transactional annotation
Annotating methods with the org.mybatis.guice.transactional.Transactional
annotation, users can eliminate recursive code patterns.
First of all, let's have a look at the injector that will create the
previous FooDAO
instance:
Class<? extends Provider<DataSource>> dataSourceProviderClass = [...];
Class<? extends Provider<TransactionFactory>> txFactoryProviderClass = [...];
Injector injector = Guice.createInjector(new MyBatisModule() {
@Override
protected void initialize() {
environmentId("test");
bindDataSourceProviderType(dataSourceProviderType);
bindTransactionFactoryType(txFactoryClass);
addMapperClass(FooMapper.class);
addMapperClass(BarMapper.class);
}
}
);
FooDAO fooDAO = injector.getInstance(FooDAO.class);
Where FooDAO
definition is:
package com.acme;
import jakarta.inject.*;
import org.apache.ibatis.session.*;
import org.mybatis.guice.transactional.*;
@Singleton
public final class FooDAOImpl {
@Inject
private FooMapper fooMapper;
@Inject
private BarMapper barMapper;
// let's assume setters here
@Transactional(
executorType = ExecutorType.BATCH,
isolation = Isolation.READ_UNCOMMITTED,
rethrowExceptionsAs = MyDaoException.class,
exceptionMessage = "Something went wrong"
)
public void doFooBar() {
this.fooMapper.doFoo();
this.barMapper.doBar();
}
}
Users can now simply read how the code can be reduced, delegating to the interceptor the session management!
The org.mybatis.guice.transactional.Transactional
annotation supports the following parameters:
Property | Default | Description |
---|---|---|
executorType | ExecutorType.SIMPLE | the MyBatis executor type |
isolation | Isolation.DEFAULT | the transaction isolation level. The default value will cause MyBatis to use the default isolation level from the data source. |
force | false | Flag to indicate that MyBatis has to force the
transaction commit() |
rethrowExceptionsAs | Exception.class | rethrow caught exceptions as new Exception (maybe a proper layer exception) |
exceptionMessage | empty string | A custom error message when throwing the custom exception;
it supports java.util.Formatter
place holders, intercepted method arguments will be used
as message format arguments. |
rollbackOnly | false | If true, the transaction will never committed, but rather the rollback will be forced. That configuration is useful for testing purposes. |
When specifying rethrowExceptionsAs
parameter,
it is required that the target exception type has the constructor
with Throwable
single argument; when specifying
both rethrowExceptionsAs
and exceptionMessage
parameters, it is required that the target exception type has the constructor
with String, Throwable
arguments;
specifying the exceptionMessage
parameter only
doesn't have any effect.
Nested transactions
The org.mybatis.guice.transactional.Transactional
annotation is nicely handled to support inner transactional methods;
given the following simple MyBatis clients:
class ServiceA {
@Transactional
public void method() {
...
}
}
class ServiceB {
@Transactional
public void method() {
...
}
}
That in a certain point are involved in another one in the same transaction:
class CompositeService {
@Inject
ServiceA serviceA;
@Inject
ServiceB serviceB;
@Transactional
public void method() {
...
this.serviceA.method();
...
this.serviceB.method();
...
}
}
In this case, ServiceA#method()
and
ServiceB#method
can be invoked as atomic transactions,
the advantage is when serviceA#method()
and
serviceB#method()
will be invoked inside the
CompositeService#method
, that the interceptor will
take care to manage them in the same session, even if annotated to
start a new transaction.
Configuration for nested transactions:
Class<? extends Provider<DataSource>> dataSourceProviderClass = [...];
Class<? extends Provider<TransactionFactory>> txFactoryProviderClass = [...];
Injector injector = Guice.createInjector(new MyBatisModule() {
@Override
protected void initialize() {
environmentId("test");
bindDataSourceProviderType(dataSourceProviderType);
bindTransactionFactoryType(txFactoryClass);
addMapperClass(FooMapper.class);
addMapperClass(BarMapper.class);
// Must bind services in the MyBatisModule for @Transactional.
bind(ServiceA.class);
bind(ServiceB.class);
bind(CompositeService.class);
}
}
);