Hot questions for Spring MyBatis

Top 10 Java Open Source / Spring / Spring MyBatis

Question:

The goal is to rollback all/any transactions in case of failure. But this doesn't work as expected.

We use Spring MVC + JMS + Service + Mybatis. In the logs, the JMS is set to rollback, but the row is inserted and not rollback. Would like to know what I'm missing or doing wrong?

The @Transactional tag was added recently. So not sure if it works as expected.

Code:

Service Class:

@Transactional(value = "transactionManager", propagation = Propagation.REQUIRED, rollbackFor = Exception.class)
public class DataExchangeLogic implements DataExchangeService {

private DataExchDao dataExchDao;

...

    @Override
    public void save(DataExch dataExch) throws ValidationException {
        if (dataExch.getId() != null && dataExch.getId() > 0) {
            this.dataExchDao.update(dataExch);
        } else {
            //LOGGER.debug("in insert::");
            this.dataExchDao.create(dataExch);
            //Empty exception throw to test rollback
            throw new RuntimeException();
        }
    }
}

DAO:

public interface DataExchDaoMybatis
extends NotificationDao {

void create(DataExch dataExch);

}

Spring Context

<bean id="dataExchLogic"  class="com.abc.service.logic.DataExchLogic">
        <property name="dataExchDao" ref="dataExchDao" />
</bean>

EAR/WAR project Spring Context

<!-- Transaction Manager -->
    <bean id="transactionManager" class="org.springframework.transaction.jta.WebSphereUowTransactionManager" />

<tx:annotation-driven transaction-manager="transactionManager" />

Logs:

[31mWARN [0;39m [36mo.s.j.l.DefaultMessageListenerContainer[0;39m # Setup of JMS message listener invoker failed for destination 'queue://REQUEST?priority=1&timeToLive=500000' - trying to recover. Cause: Transaction rolled back because it has been marked as rollback-only 
org.springframework.transaction.UnexpectedRollbackException: Transaction rolled back because it has been marked as rollback-only
    at org.springframework.transaction.support.AbstractPlatformTransactionManager.commit(AbstractPlatformTransactionManager.java:720)
    at org.springframework.jms.listener.AbstractPollingMessageListenerContainer.receiveAndExecute(AbstractPollingMessageListenerContainer.java:240)
    at org.springframework.jms.listener.DefaultMessageListenerContainer$AsyncMessageListenerInvoker.invokeListener(DefaultMessageListenerContainer.java:1142)
    at org.springframework.jms.listener.DefaultMessageListenerContainer$AsyncMessageListenerInvoker.executeOngoingLoop(DefaultMessageListenerContainer.java:1134)
    at org.springframework.jms.listener.DefaultMessageListenerContainer$AsyncMessageListenerInvoker.run(DefaultMessageListenerContainer.java:1031)
    at java.lang.Thread.run(Thread.java:745)
[34mINFO [0;39m [36mo.s.j.l.DefaultMessageListenerContainer[0;39m # Successfully refreshed JMS Connection 
[39mDEBUG[0;39m [36mo.s.j.l.DefaultMessageListenerContainer[0;39m # Received message of type [class com.ibm.ws.sib.api.jms.impl.JmsTextMessageImpl] from consumer [com.ibm.ws.sib.api.jms.impl.JmsQueueReceiverImpl@6ca01c74] of transactional session [com.ibm.ws.sib.api.jms.impl.JmsQueueSessionImpl@3ac3b63] 
Creating a new SqlSession
Registering transaction synchronization for SqlSession [org.apache.ibatis.session.defaults.DefaultSqlSession@206ee277]
JDBC Connection [com.ibm.ws.rsadapter.jdbc.WSJdbcConnection@19b89f0c] will be managed by Spring
[39mDEBUG[0;39m [36mg.c.i.q.d.m.N.create!selectKey[0;39m # ==>  Preparing: SELECT ID.NEXTVAL FROM DUAL  
[39mDEBUG[0;39m [36mg.c.i.q.d.m.N.create!selectKey[0;39m # ==> Parameters:  
[39mDEBUG[0;39m [36mg.c.i.q.d.m.N.create!selectKey[0;39m # <==      Total: 1 
[39mDEBUG[0;39m [36mg.c.i.q.d.m.N.create[0;39m # ==>  Preparing: INSERT INTO TABLE ( COL1, COL2, COL N) VALUES ( ?, CURRENT_TIMESTAMP, ?, ?, ?, ?, ?, ?)  
[39mDEBUG[0;39m [36mg.c.i.q.d.m.N.create[0;39m # ==> Parameters: 468(Integer), SYSTEM(String), 2017-03-01 00:00:00.0(Timestamp), 2017-03-16 00:00:00.0(Timestamp), true(Boolean), test 112(String), ALL(String) 
[39mDEBUG[0;39m [36mg.c.i.q.d.m.N.create[0;39m # <==    Updates: 1 
Releasing transactional SqlSession [org.apache.ibatis.session.defaults.DefaultSqlSession@206ee277]
Transaction synchronization deregistering SqlSession [org.apache.ibatis.session.defaults.DefaultSqlSession@206ee277]
Transaction synchronization closing SqlSession [org.apache.ibatis.session.defaults.DefaultSqlSession@206ee277]

EDIT 1:

Controller code:

@ResourceMapping(value = "addNewDEURL")
    public void addNewDE(@ModelAttribute(value = "dataObject") final DataExch dataExch,
                                   final BindingResult bindingResult, final ResourceResponse response) {
        if (!bindingResult.hasErrors()) {

            try {
                dataExchangeService.save(dataExch);
            } catch (final ValidationException e) {
                logger.error("A validation exception occurred.", e);
            }                           
        } else {
            logger.error(bindingResult.getAllErrors().get(0)
                .getDefaultMessage());
        }
    }

DAO changed:

public class DataExchDaoMybatis extends BaseDaoImpl implements DataExchDao {

public void create(DataExch dataExch) {
        doSimpleInsert("insertDE", dataExch);
    }
}

BaseDaoImpl:

public void doSimpleInsert(String queryId, Object o) {
        SqlSession sqlSession = sqlSessionFactory.openSession();
        sqlSession.insert(queryId, o);
}

Answer:

Please put transactionManager configuration and tx:annotation-driven into root spring context

Rule: Root context can see all the beans which Spring created. Child context(any Web Context) can see only its own beans.

In this particular case tx:annotation-driven looks for beans with @Transactional annotation in Web context. It cannot find any because you defined dataExchLogic in root context. That's why you didn't have any transactional behavior.

@EnableTransactionManagement and only looks for @Transactional on beans in the same application context they are defined in. This means that, if you put annotation driven configuration in a WebApplicationContext for a DispatcherServlet, it only checks for @Transactional beans in your controllers, and not your services. See Section 21.2, “The DispatcherServlet” for more information.

Solution implies to move tx:annotation-driven to the root context because Root Context can find any bean defined either in root or in any web context.

Question:

I have a simple spring-boot-mybatis app (keep in mind, please). Mybatis is logging SQL queries only in case of failure (on excepions). Tell me please, how to force it to log all SQL query to console ?

At this moment I am using slf4j logger (automatically configured by spring-boot). I find this link: http://www.mybatis.org/mybatis-3/logging.html however I didnt manage to follow it. First of all configuration is shown for log4j, and I am not sure If I correctly understand: Is it sufficient to configure in application.properties ?

Thanks in advance


Answer:

Spring boot uses logback as default logging provider for Slf4j. Ibatis internal log factory loads the SLF4j as the first choice logger. All you have to do is configure your spring boot logger to publish log messages for ibatis mapper.

Add the below lines in boot application properties.

logging.level.org.springframework=WARN
logging.level.com.spring.ibatis.UserMapper=DEBUG
logging.file=logs/spring-boot-logging.log

The second line is where you define the logging entry for ibatis mapper with DEBUG log level. com.spring.ibatis is package and the UserMapper is sample mapper.

The following logs will start to appear in the console and in the spring-boot-logging file. These are the log messages generated from saveUser and findByName method of ApplicationTest class.

2016-12-19 22:07:06.358  INFO 7248 --- [main] com.spring.ibatis.ApplicationTest        : Started ApplicationTest in 3.048 seconds (JVM running for 4.209)
2016-12-19 22:07:06.424 DEBUG 7248 --- [main] com.spring.ibatis.UserMapper.saveUser    : ==>  Preparing: insert into users(name) values(?) 
2016-12-19 22:07:06.444 DEBUG 7248 --- [main] com.spring.ibatis.UserMapper.saveUser    : ==> Parameters: ibatis(String)
2016-12-19 22:07:06.445 DEBUG 7248 --- [main] com.spring.ibatis.UserMapper.saveUser    : <==    Updates: 1
2016-12-19 22:07:06.457 DEBUG 7248 --- [main] com.spring.ibatis.UserMapper.findByName  : ==>  Preparing: select name from users WHERE name=? 
2016-12-19 22:07:06.470 DEBUG 7248 --- [main]  com.spring.ibatis.UserMapper.findByName  : ==> Parameters: ibatis(String)
2016-12-19 22:07:06.504 DEBUG 7248 --- [main]  com.spring.ibatis.UserMapper.findByName  : <==      Total: 1

You can of course configure any choice of logger you want. I can easily add an example for any other logger should you need.

You can find the complete code with Junit test cases at https://github.com/saagar2000/ibatis

Question:

Today I was preparing an example using Spring Boot and using MyBatis for the data access communication next to Spring-MyBatis. Here is the relevant project configuration (using maven):

src/main/java
- edu.home.ltmj.controller
  + CategoryController.java
- edu.home.ltmj.dao
  + CategoryDao.java
- edu.home.ltmj.domain
  + Category.java
src/main/resources
- edu.home.ltmj.dao
  + CategoryMapper.xml

Relevant content of the files:

CategoryDao.java:

package edu.home.ltmj.dao;

public interface CategoryDao {
    List<Category> getAllCategories();
}

CategoryMapper.xml:

<mapper namespace="edu.home.ltmj.dao.CategoryDao">
    <resultMap id="categoryMap"
        type="edu.home.ltmj.domain.Category">
        <id property="id" column="id" />
        <result property="name" column="name" />
    </resultMap>
    <select id="getAllCategories" resultMap="categoryMap">
        SELECT id, nombre
        FROM category
    </select>
</mapper>

Then, I inject an instance of this dao in a request controller (for testing purposes), like this:

package edu.home.ltmj.controller;

@RestController
public class CategoryController {
    @Autowired
    private CategoryDao dao;

    @RequestMapping(value="/category/all",
        method=RequestMethod.GET,
        produces=MediaType.APPLICATION_JSON_VALUE)
    public List<Categoria> getAllCategories() {
        return dao.getAllCategories();
    }
}

I run my project and test the execution by using curl localhost:8080/category/all and then expected to see the results in JSON format, but I got this exception instead:

org.apache.ibatis.binding.BindingException: Invalid bound statement (not found): edu.home.ltmj.dao.CategoryDao.getAllCategories
at org.apache.ibatis.binding.MapperMethod$SqlCommand.<init>(MapperMethod.java:189)
at org.apache.ibatis.binding.MapperMethod.<init>(MapperMethod.java:43)
at org.apache.ibatis.binding.MapperProxy.cachedMapperMethod(MapperProxy.java:58)
at org.apache.ibatis.binding.MapperProxy.invoke(MapperProxy.java:51)
at com.sun.proxy.$Proxy45.getAllCategories(Unknown Source)
at edu.home.ltmj.controller.CategoryRestController.getAllCategories(CategoryRestController.java:27)
at sun.reflect.NativeMethodAccessorImpl.invoke0(Native Method)
(...)

I don't understand the cause of this. There's an interface CategoryDao and it has the proper method getAllCategories that matches with <select id="getAllCategories">. After some time of playing with this, I've changed the name of the dao interface to CategoryMapper and updated the namespace in CategoryMapper.xml. After I did this, everything worked normally. Also, after having the same name for class and xml, I moved the dao class and the xml mapper into different packages (stil using the same name for both: CategoryMapper.), updated the namespace in the xml file, and got the same exception, with the message updated to show the name of the package of the dao interface. But then again, I moved both files to the same package and everything worked again.

So, my question is: why does MyBatis need that the interface and the xml mapper file to have the same name and be in the same package? Is this MyBatis design or an issue in Spring MyBatis?


Answer:

Do you also have a MyBatis Config file?

If I remember correctly the same name same location for the XML file as the interface is when you want to have a setup that just works with no extra configuration.

If you have the XML mappers somewhere else you can manually specify the classpath of the XML files using a <mappers> element inside MyBatis configuration.

From the Injecting Mappers documentation:

If the UserMapper has a corresponding MyBatis XML mapper file in the same classpath location as the mapper interface, it will be parsed automatically by the MapperFactoryBean. There is no need to specify the mapper in a MyBatis configuration file unless the mapper XML files are in a different classpath location. See the SqlSessionFactoryBean's configLocation property for more information.

So try this:

  1. Create a mybatis-config.xml file inside src/main/resources with this in it:

    <?xml version="1.0" encoding="UTF-8" ?>
    <!DOCTYPE configuration
      PUBLIC "-//mybatis.org//DTD Config 3.0//EN"
      "http://mybatis.org/dtd/mybatis-3-config.dtd">
    <configuration>
      <mappers>
        <mapper resource="com/test/path/etc/etc/WhateverNameYouWant.xml"/>
      </mappers>
    </configuration>
    

    Where WhateverNameYouWant.xml contains what your CategoryMapper.xml contained.

  2. Set the location of the config file (Java configuration as below or bean in the applicationContext file):

    @Bean
    public SqlSessionFactoryBean sqlSessionFactory() throws Exception {
        SqlSessionFactoryBean sessionFactory = new SqlSessionFactoryBean();
        // ....
        sessionFactory.setConfigLocation(new ClassPathResource("mybatis-config.xml"));
        // ....
        return sessionFactory;
    }
    

Question:

I had the following error messages thrown when registering the mapper classes on my server startup,

[artifact:mvn] 2016-05-07 11:39:21,708 [ERROR] org.mybatis.spring.mapper.MapperFactoryBean - Error while adding the mapper 'interface com.sample.mappers.UserMapper' to configuration.
[artifact:mvn] java.lang.IllegalArgumentException: Mapped Statements collection already contains value for com.sample.mappers.UserMapper.getAllUsers
[artifact:mvn]  at org.apache.ibatis.session.Configuration$StrictMap.put(Configuration.java:802)
[artifact:mvn]  at org.apache.ibatis.session.Configuration$StrictMap.put(Configuration.java:774)
[artifact:mvn]  at org.apache.ibatis.session.Configuration.addMappedStatement(Configuration.java:598)
[artifact:mvn]  at org.apache.ibatis.builder.MapperBuilderAssistant.addMappedStatement(MapperBuilderAssistant.java:300)
[artifact:mvn]  at org.apache.ibatis.builder.annotation.MapperAnnotationBuilder.parseStatement(MapperAnnotationBuilder.java:313)
[artifact:mvn]  at org.apache.ibatis.builder.annotation.MapperAnnotationBuilder.parse(MapperAnnotationBuilder.java:128)
[artifact:mvn]  at org.apache.ibatis.binding.MapperRegistry.addMapper(MapperRegistry.java:72)
[artifact:mvn]  at org.apache.ibatis.session.Configuration.addMapper(Configuration.java:671)
[artifact:mvn]  at org.mybatis.spring.mapper.MapperFactoryBean.checkDaoConfig(MapperFactoryBean.java:81)
[artifact:mvn]  at org.springframework.dao.support.DaoSupport.afterPropertiesSet(DaoSupport.java:44)

I used annotations for my mapper interfaces and there's no xml configuration.

Below is my UserMapper interface,

public interface UserMapper {

  @Select("SELECT  * FROM customer")
  List<User> getAllUsers();

  @Select("SELECT * FROM customer where userId = #{userId} ")
  List<User> getAllUsers(Long userId);

}

Answer:

I found out the cause of the error message. If you have the same method name, then mybatis throws the Mapped Statements collection already contains value error message. So the solution is to have different method names for different mapper statements even if the method signatures are different.

So In my mapper interface the method names second getAllUsers() name should be getUserById();. The same error is thrown if you have the same method name in any of the mapper.xml files. So it is mandatory to have unique method names or mapper namespace for different sql statements for mybatis to map this at runtime.

Question:

I've been using Spring with MyBatis and it's been working really well for a single database. I ran into difficulties when trying to add another database (see reproducible example on Github).

I'm using Spring Java configuration (i.e. not XML). Most of the examples I've seen show how to achieve this using XML.

I have two data configuration classes (A & B) like this:

@Configuration
@MapperScan("io.woolford.database.mapper")
public class DataConfigDatabaseA {

    @Bean(name="dataSourceA")
    public DataSource dataSourceA() throws SQLException {
        SimpleDriverDataSource dataSource = new SimpleDriverDataSource();
        dataSource.setDriver(new com.mysql.jdbc.Driver());
        dataSource.setUrl("jdbc:mysql://" + dbHostA + "/" + dbDatabaseA);
        dataSource.setUsername(dbUserA);
        dataSource.setPassword(dbPasswordA);
        return dataSource;
    }

    @Bean
    public SqlSessionFactory sqlSessionFactory() throws Exception {
        SqlSessionFactoryBean sessionFactory = new SqlSessionFactoryBean();
        sessionFactory.setDataSource(dataSourceA());
        return sessionFactory.getObject();
    }
}

Two mappers, and a service that autowires the mappers:

@Service
public class DbService {

    @Autowired
    private DbMapperA dbMapperA;

    @Autowired
    private DbMapperB dbMapperB;

    public List<Record> getDabaseARecords(){
        return dbMapperA.getDatabaseARecords();
    }

    public List<Record> getDabaseBRecords(){
        return dbMapperB.getDatabaseBRecords();
    }

}

The application won't start:

Error creating bean with name 'dataSourceInitializer': 
  Invocation of init method failed; nested exception is 
    org.springframework.beans.factory.NoUniqueBeanDefinitionException: 
      No qualifying bean of type [javax.sql.DataSource] is defined: 
        expected single matching bean but found 2: dataSourceB,dataSourceA

I've read that it's possible to use the @Qualifier annotation to disambiguate the autowiring, though I wasn't sure where to add it.

Can you see where I'm going wrong?


Answer:

If you want to use two data sources at same time and they are not primary and secondary, you should disable DataSourceAutoConfiguration by @EnableAutoConfiguration(excludes = {DataSourceAutoConfiguration.class}) on your application annotated by @SpringBootApplication. Afterwards, you can create your own SqlSessionFactory and bundle your own DataSource. If you also want to use DataSourceTransactionManager, you should do that too.

In this case, you haven't disabled DataSourceAutoConfiguration, so spring framework will try to @Autowired only one DataSource but got two, error occurs.

As what I've said before, you should disable DataSourceAutoConfiguration and configure it manually.

You can disable data source auto configuration as following:

@SpringBootApplication
@EnableAutoConfiguration(exclude = {DataSourceAutoConfiguration.class})
public class YourApplication implements CommandLineRunner {
    public static void main (String... args) {
        SpringApplication.run(YourApplication.class, args);
    }
}

And if you are really want to use multiple databases at same time, I suggest you to registering proper bean manually, such as:

package xyz.cloorc.boot.mybatis;

import org.apache.commons.dbcp.BasicDataSource;
import org.apache.ibatis.session.SqlSessionFactory;
import org.mybatis.spring.SqlSessionFactoryBean;
import org.mybatis.spring.support.SqlSessionDaoSupport;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.stereotype.Repository;

import javax.annotation.PostConstruct;
import javax.annotation.Resource;
import javax.sql.DataSource;

@Configuration
public class SimpleTest {

    private DataSource dsA;
    private DataSource dsB;

    @Bean(name = "dataSourceA")
    public DataSource getDataSourceA() {
        return dsA != null ? dsA : (dsA = new BasicDataSource());
    }

    @Bean(name = "dataSourceB")
    public DataSource getDataSourceB() {
        return dsB != null ? dsB : (dsB = new BasicDataSource());
    }

    @Bean(name = "sqlSessionFactoryA")
    public SqlSessionFactory getSqlSessionFactoryA() throws Exception {
        // set DataSource to dsA
        return new SqlSessionFactoryBean().getObject();
    }

    @Bean(name = "sqlSessionFactoryB")
    public SqlSessionFactory getSqlSessionFactoryB() throws Exception {
        // set DataSource to dsB
        return new SqlSessionFactoryBean().getObject();
    }
}

@Repository
public class SimpleDao extends SqlSessionDaoSupport {

    @Resource(name = "sqlSessionFactoryA")
    SqlSessionFactory factory;

    @PostConstruct
    public void init() {
        setSqlSessionFactory(factory);
    }

    @Override
    public void setSqlSessionFactory(SqlSessionFactory sqlSessionFactory) {
        super.setSqlSessionFactory(sqlSessionFactory);
    }

    public <T> T get (Object id) {
        return super.getSqlSession().selectOne("sql statement", "sql parameters");
    }
}