Why must the interface and xml mapper file be in same package and have the same name?

Luiggi Mendoza picture Luiggi Mendoza · May 15, 2015 · Viewed 13.9k times · Source

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

Bogdan picture Bogdan · May 16, 2015

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;
    }