I’m playing with technologies and framework for a new project: because I’m learning things probably useful for others, I’m gonna write here few notes about what I’ve learned. This is the sixth article of a series: we need a way to access the MyBatis session (to flush the statement cache) in mappers/DAOs.

Part 6 – Accessing the MyBatis session to execute flushStatements()

In the previous article, I’ve created a DAO to insert a collection of record in a transaction. The loops inside the replace() method are quite naive: when you deal with a huge number of records, actually you need to call SqlSession#flushStatement() every now and then, to execute the cached statements and free some memory.

Let’s see how it can be done with mybatis-spring (with a bit of troubleshooting)!

Technologies used:

  • Java 8
  • MyBatis 3.3
  • Spring framework 4.2
  • Mybatis-Spring 1.2.3
  • MyBatis Generator 1.3.3 snapshot

Step 1 – Giving DAO the access to the session…

In mybatis-spring, there is a way to give DAO class the access to the current SqlSession: it have to extend the SqlSessionDaoSupport abstract class (which extends DaoSupport class shipped by Spring).

The previous Tbit88HpDaoImpl should be modified as follows.

@Repository("tbit88HpDao")
public class Tbit88HpDaoImpl extends SqlSessionDaoSupport implements Tbit88HpDao {

  @Autowired
  private Tbit88HpMapper mapper;

  @Override
  @Transactional
  public int replace(List<Tbit88Hp> records) {
    int count = 0;
    for (Tbit88Hp record : records) {
      mapper.deleteByPrimaryKey(record);
      if (++count % 10000 == 0) {
        getSqlSession().flushStatements();
      }
    }
    count = 0;
    for (Tbit88Hp record : records) {
      mapper.insert(record);
      if (++count % 10000 == 0) {
        getSqlSession().flushStatements();
      }
    }
    return count;
  }
}

SqlSessionDaoSupport needs either a SqlSessionTemplate or a SqlSessionFactory, but they are not autowired. The mybatis-spring documentation shows how to configure that with XML configuration.

Instead I’ve done it with a little bit of Java code (for every DAO that extends SqlSessionDaoSupport!): we need to define a new BeanPostProcessor and instantiate it with a @Bean annotated method.

public class MybatisSpringBeanPostProcessor implements BeanPostProcessor, ApplicationContextAware {

  private ApplicationContext context;

  @Override
  public Object postProcessBeforeInitialization(Object bean, String beanName) throws BeansException {
    if (bean instanceof SqlSessionDaoSupport) {
      ((SqlSessionDaoSupport) bean).setSqlSessionFactory(context.getBean(SqlSessionFactory.class));
      //((SqlSessionDaoSupport) bean).setSqlSessionTemplate(context.getBean(SqlSessionTemplate.class));
    } else if (bean instanceof MapperFactoryBean) {
      ((MapperFactoryBean<?>) bean).setSqlSessionTemplate(context.getBean(SqlSessionTemplate.class));
    }
    return bean;
  }

  @Override
  public Object postProcessAfterInitialization(Object bean, String beanName) throws BeansException {
    return bean;
  }

  @Override
  public void setApplicationContext(ApplicationContext applicationContext) throws BeansException {
    this.context = applicationContext;
  }
}
@Configuration
public class MyBeanPostProcessorConfig {

  @Bean
  public static MybatisSpringBeanPostProcessor mybatisSpringBeanPostProcessor() {
    return new MybatisSpringBeanPostProcessor();
  }
}

It looked great at a first glance, but…

Step 2 – …has a problem

When running the application, the Spring initialization log a series of “INFO” messages, similar to this one:

Bean '...' of type [...] is not eligible for getting processed by all BeanPostProcessors (for example: not eligible for auto-proxying)

This message was repeated for every mapper instance and few other classes.

Our custom BeanPostProcessor needs that many classes are early instantiated… too early for the Spring’s transaction management (in fact, removing @EnableTransactionManagement makes the messages to disappear). This page describes deeply the problem; there is an open jira issue too.

The test application works, but who knows what can happen in a real production environment?

Step 3 – A possible solution

I need to access the MyBatis SqlSession only to call flushStatements: I found another solution for this scenario.

First, remove all modifications done in the previous steps. Then create a “root interface” for every mapper you need to have access to the flushStatements method.

package it.caladyon.db.support;
import java.util.List;
import org.apache.ibatis.annotations.Flush;
import org.apache.ibatis.executor.BatchResult;

public interface FlushableMapper {
  @Flush
  public List<BatchResult> flush();
}

 

Even if I’m quite fond of XML-based mapper, I have to use this annotation because this can be done only with the MyBatis Java API.

You need to configure MyBatis Generator, using the “rootInterface” property (either globally in the javaClientGenerator tag or per table).

<property name="rootInterface" value="it.caladyon.db.support.FlushableMapper"/>

So, the DAO implementation could be written this way:

@Repository("tbit88HpDao")
public class Tbit88HpDaoImpl implements Tbit88HpDao {

  @Override
  @Transactional
  public int replace(List<Tbit88Hp> records) {
    int count = 0;
    for (Tbit88Hp record : records) {
      mapper.deleteByPrimaryKey(record);
      if (++count % 10000 == 0) {
        mapper.flush();
      }
    }
    count = 0;
    for (Tbit88Hp record : records) {
      mapper.insert(record);
      if (++count % 10000 == 0) {
        mapper.flush();
      }
    }
    return count;
  }
}

No need to access the MyBatis session, no alerts in the log.

 

 

Annunci