mercoledì 28 settembre 2016

MyBatis + Guice and the Transactional annotation

I wasn't a huge fan of MyBatis, mainly because I hate working with XML. For the project we are working on, we wanted to use the same objects for persistence and to pass as messages to an actor system (Akka) to do some work in parallel, so we really wanted our data objects to be immutable, this removed Hibernate from the picture.
MyBatis turned our to be very reliable and not as painful as I thought when it came to creating the mappers.
As a dependency injection we decided to use Guice, and the integration between the two proved to be seamless. The only problems started showing up when we created some integrations tests to check whether the Transactional annotations were working. The bugs were many, and hard to debug, so I decided to create a dummy project to test how the Transactional annotation works once and for all.
In our project, we are using different databases in the application so we have to use private modules, which introduces a whole bunch of caveats, but before digging into it, let's start with what the internet says about this.


The Transactional annotation documentation

The official Guice wiki doesn't say much about what affect the Transactional annotation, only that:


The only restriction is that these methods must be on objects that were created by Guice and they must not be private.

Fair enough. MyBatis-Guice's documentation ads some more informations about nested transactions, but still doesn't mention issues with private modules. The documentation is very poor for such an important piece of the library 


Setting up the environment

For these tests, I created a simple environment: a MyBatis mapper that doesn't really do anything, a service implementing an interface in which I inject the mapper. I also inject the SqlSessionManager in the service to check whether the method is being executed inside a transaction by calling sqlSessionManager.isManagedSessionStarted().

This is the mapper:


package com.mirkorossini.transactionaltest.mappers;
import org.apache.ibatis.annotations.Insert;
import org.apache.ibatis.annotations.Param;
public interface DummyMapper {
@Insert("insert into dummytable values (#{value})")
void insertRecord(@Param("value") int value);
}

This is the service interface:



package com.mirkorossini.transactionaltest.services;
public interface DummyServiceInt {
void insertValue(int value);
}
This is the service:
package com.mirkorossini.transactionaltest.services;
import com.mirkorossini.transactionaltest.mappers.DummyMapper;
import org.apache.ibatis.session.SqlSessionManager;
import org.mybatis.guice.transactional.Transactional;
import javax.inject.Inject;
public class DummyService implements DummyServiceInt {
private final DummyMapper dummyMapper;
private final SqlSessionManager sqlSessionManager;
@Inject
public DummyService(final DummyMapper dummyMapper,
final SqlSessionManager sqlSessionManager) {
this.dummyMapper = dummyMapper;
this.sqlSessionManager = sqlSessionManager;
}
@Override
@Transactional
public void insertValue(int value) {
assert sqlSessionManager.isManagedSessionStarted();
}
public void insertValueCallingPublicMethod(int value) {
insertValue(value);
}
public void insertValueCallingPrivateMethod(int value) {
insertValuePrivate(value);
}
@Transactional
private void insertValuePrivate(int value) {
assert sqlSessionManager.isManagedSessionStarted();
}
}



The tests


The tests simply create an injector, get an instance of the service and call addValue on it. The method will throw an AssertionError if a transaction was not started, indicating that the Transactional annotation was ignored. 
Let's ignore the interface for now and focus on the DummyService implementation.
For the first test I just create a normal MyBatis module and bind the DummyMapper. This test passes.

@Test
public void testBindMapper() {
final Module myBatisModule = new MyBatisModule() {
@Override
protected void initialize() {
environmentId(ENV);
install(JdbcHelper.H2_EMBEDDED);
bindDataSourceProviderType(PooledDataSourceProvider.class);
bindTransactionFactoryType(JdbcTransactionFactory.class);
addMapperClass(DummyMapper.class);
}
};
final Injector injector = Guice.createInjector(
Stage.DEVELOPMENT, new Module[]{myBatisModule});
injector.getInstance(DummyService.class).insertValue(1);
}
view raw .java hosted with ❤ by GitHub


Now, let's try to run a public method calling a private method annotated with Transactional. As the Guice documentation mentioned, this test fails.
@Test
public void testBindMapperPrivateMethod() {
/**
* Won't work: mybatis doesn't know about the service
*/
final Module myBatisModule = new MyBatisModule() {
@Override
protected void initialize() {
environmentId(ENV);
install(JdbcHelper.H2_EMBEDDED);
bindDataSourceProviderType(PooledDataSourceProvider.class);
bindTransactionFactoryType(JdbcTransactionFactory.class);
addMapperClass(DummyMapper.class);
}
};
final Injector injector = Guice.createInjector(
Stage.DEVELOPMENT, new Module[]{myBatisModule});
injector.getInstance(DummyService.class).insertValueCallingPrivateMethod(1);
}
view raw .java hosted with ❤ by GitHub

Let's add private modules to the picture. I will bind the mapper and expose the mapper in a private service and then try to get an instance of the DummyService. This doesn't work, as the module has to know about the service for the Transactional annotation to work.

@Test
public void testBindMapperPrivateModule() {
final Module privateModule = new PrivateModule() {
@Override
protected void configure() {
final Module myBatisModule = new MyBatisModule() {
@Override
protected void initialize() {
environmentId(ENV);
install(JdbcHelper.H2_EMBEDDED);
bindDataSourceProviderType(PooledDataSourceProvider.class);
bindTransactionFactoryType(JdbcTransactionFactory.class);
addMapperClass(DummyMapper.class);
}
};
install(myBatisModule);
expose(DummyMapper.class);
expose(SqlSessionManager.class);
}
};
final Injector injector = Guice.createInjector(
Stage.DEVELOPMENT, privateModule);
injector.getInstance(DummyService.class).insertValue(1);
}
view raw .java hosted with ❤ by GitHub

To make it work, you need to expose the DummyService explicitely:

@Test
public void testBindMapperPrivateModuleExposedService() {
/**
*/
final Module privateModule = new PrivateModule() {
@Override
protected void configure() {
final Module myBatisModule = new MyBatisModule() {
@Override
protected void initialize() {
environmentId(ENV);
install(JdbcHelper.H2_EMBEDDED);
bindDataSourceProviderType(PooledDataSourceProvider.class);
bindTransactionFactoryType(JdbcTransactionFactory.class);
addMapperClass(DummyMapper.class);
}
};
install(myBatisModule);
bind(DummyService.class);
expose(DummyService.class);
}
};
final Injector injector = Guice.createInjector(
Stage.DEVELOPMENT, privateModule);
injector.getInstance(DummyService.class).insertValue(1);
}
view raw .java hosted with ❤ by GitHub

Now let's see what happens with interfaces. Let's try binding and exposing the interface instead of the implementation directly, and see if the private module method still works (it does)

@Test
public void testBindMapperPrivateModuleExposedInterface() {
/**
*/
final Module privateModule = new PrivateModule() {
@Override
protected void configure() {
final Module myBatisModule = new MyBatisModule() {
@Override
protected void initialize() {
environmentId(ENV);
install(JdbcHelper.H2_EMBEDDED);
bindDataSourceProviderType(PooledDataSourceProvider.class);
bindTransactionFactoryType(JdbcTransactionFactory.class);
addMapperClass(DummyMapper.class);
}
};
install(myBatisModule);
bind(DummyServiceInt.class).to(DummyService.class);
expose(DummyServiceInt.class);
}
};
final Injector injector = Guice.createInjector(
Stage.DEVELOPMENT, privateModule);
injector.getInstance(DummyServiceInt.class).insertValue(1);
}
view raw .java hosted with ❤ by GitHub

So far, everything is working as expected (more or less), but then something weird happened. I had a bug I was trying to fix and I was growing desperate, I tried to explicitely expose Mapper and SqlSessionManager on top of the DummyService, to get closer to our product. I thought it wouldn't make a difference but it did, and this test fails:
@Test
public void testBindMapperPrivateModuleExposedInterfaceExposeSqlSessionManager() {
/**
*/
final Module privateModule = new PrivateModule() {
@Override
protected void configure() {
final Module myBatisModule = new MyBatisModule() {
@Override
protected void initialize() {
environmentId(ENV);
install(JdbcHelper.H2_EMBEDDED);
bindDataSourceProviderType(PooledDataSourceProvider.class);
bindTransactionFactoryType(JdbcTransactionFactory.class);
addMapperClass(DummyMapper.class);
}
};
install(myBatisModule);
bind(DummyServiceInt.class).to(DummyService.class);
expose(DummyServiceInt.class);
expose(SqlSessionManager.class);
expose(DummyMapper.class);
}
};
final Injector injector = Guice.createInjector(
Stage.DEVELOPMENT, privateModule);
injector.getInstance(DummyServiceInt.class).insertValue(1);
}
view raw .java hosted with ❤ by GitHub

Even stranger, it doesn't fail if I bind and expose the implementation directly:

@Test
public void testBindMapperPrivateModuleExposeSqlSessionManager() {
/**
*/
final Module privateModule = new PrivateModule() {
@Override
protected void configure() {
final Module myBatisModule = new MyBatisModule() {
@Override
protected void initialize() {
environmentId(ENV);
install(JdbcHelper.H2_EMBEDDED);
bindDataSourceProviderType(PooledDataSourceProvider.class);
bindTransactionFactoryType(JdbcTransactionFactory.class);
addMapperClass(DummyMapper.class);
}
};
install(myBatisModule);
bind(DummyService.class);
expose(DummyService.class);
expose(SqlSessionManager.class);
expose(DummyMapper.class);
}
};
final Injector injector = Guice.createInjector(
Stage.DEVELOPMENT, privateModule);
injector.getInstance(DummyService.class).insertValue(1);
}
view raw .java hosted with ❤ by GitHub
I probably stumbled upon a weird bug, I will update this post if something comes up, but the documentation on this topic is seriously lacking.

This test, farther away from our implementation, reproduces the issue more clearly.

package com.mirkorossini.transactionaltest;
import com.google.inject.*;
import org.apache.ibatis.session.SqlSessionManager;
import org.apache.ibatis.transaction.jdbc.JdbcTransactionFactory;
import org.mybatis.guice.MyBatisModule;
import org.mybatis.guice.datasource.builtin.PooledDataSourceProvider;
import org.mybatis.guice.datasource.helper.JdbcHelper;
import org.mybatis.guice.transactional.Transactional;
import javax.inject.Inject;
public class MyBatisTransactionalTest {
private static final String ENV = "test";
private final static Module MY_BATIS_MODULE = new MyBatisModule() {
@Override
protected void initialize() {
environmentId(ENV);
install(JdbcHelper.H2_EMBEDDED);
bindDataSourceProviderType(PooledDataSourceProvider.class);
bindTransactionFactoryType(JdbcTransactionFactory.class);
}
};
public interface MyInterface {
boolean isTransactional();
}
public static class MyImplementation implements MyInterface {
private final SqlSessionManager sqlSessionManager;
@Inject
public MyImplementation(final SqlSessionManager sqlSessionManager
) {
this.sqlSessionManager = sqlSessionManager;
}
@Transactional
@Override
public boolean isTransactional() {
return sqlSessionManager.isManagedSessionStarted();
}
}
private static void testTransactional(final Module privateModule) {
final Injector injector = Guice.createInjector(
Stage.DEVELOPMENT, privateModule);
if (!injector.getInstance(MyImplementation.class).isTransactional()) {
throw new RuntimeException("Not transactional");
};
}
public static void main (String[] args) {
final Module privateModule = new PrivateModule() {
@Override
protected void configure() {
install(MY_BATIS_MODULE);
bind(MyImplementation.class);
expose(MyImplementation.class);
expose(SqlSessionManager.class);
}
};
testTransactional(privateModule);
final Module privateModuleBindingTheinterface = new PrivateModule() {
@Override
protected void configure() {
install(MY_BATIS_MODULE);
bind(MyInterface.class).to(MyImplementation.class);
expose(MyInterface.class);
expose(SqlSessionManager.class);
}
};
testTransactional(privateModuleBindingTheinterface);
}
}



Update


The MyBatis/Guice team promptly got back to me, apparently it's a known Guice issue, but it's "expected behavior". Pretty much what's happening is that Guice is instantiating the service in the private module's parent, and the annotations defined in the private module (including Transactional) are ignored. Suggested workarounds: bind instances directly when they use the Transactional annotation, or use requireExplicitBindings()