@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:

org.mybatis.guice.transactional.Transactional properties
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);
        }

    }
);