Thursday, February 17, 2011

Integrate MyBatis 3.x with Spring 3.x

Since last I wrote about integrating Spring with iBatis, and about using the very useful auto-rollback in Spring Test Framework, things have changed. The iBatis project has shifted to Google and is now called MyBatis, and includes numerous API changes. But, this was done after Spring 3.0 was released - so full integration with Spring was left undone at that time. However, a Spring-MyBatis integration component is now available, and here I'll write up how to use it to very simply use MyBatis, preserving the auto-rollback feature.

Here's the Spring-MyBatis POM entry:


<dependency>
<groupId>org.mybatis</groupId>
<artifactId>mybatis-spring</artifactId>
<version>1.0.0</version>
</dependency>

Next, you'll need these pieces:

  1. Spring context file
  2. A Java interface that presents the necessary queries as methods
  3. An application component (e.g. a DAO) that uses the Java interface
  4. A test that test-drives all of the above

You no longer need the configuration and mapper files; while you still could do it that way, the example here will demonstrate use of annotation to define the queries in a Java interface, and a simple Spring entry to point to the Java interface.

The Spring context file needs to define the following:

  1. Data Source
  2. SQL Session Factory
  3. MyBatis Mapper Factory Bean
  4. Spring transaction manager (to facilitate the auto-rollback)
  5. Your application DAO

Here's an example:


<beans xmlns="http://www.springframework.org/schema/beans"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns:context="http://www.springframework.org/schema/context"
xsi:schemaLocation="http://www.springframework.org/schema/beans
http://www.springframework.org/schema/beans/spring-beans-3.0.xsd
http://www.springframework.org/schema/context
http://www.springframework.org/schema/context/spring-context-3.0.xsd">

<bean id="datasource" class="oracle.jdbc.pool.OracleDataSource">
<property name="dataSourceName" value="ds"/>
<!-- use property-placeholder (e.g.) to fill in the database connection variables -->
<property name="URL" value="jdbc:oracle:thin:@//${dbhost}:${dbport}/${dbname}"/>
<property name="user" value="${dbuser}"/>
<property name="password" value="${dbpassword}"/>
</bean>

<bean id="sqlSessionFactory" class="org.mybatis.spring.SqlSessionFactoryBean">
<property name="dataSource" ref="datasource"/>
</bean>

<bean id="myMapper" class="org.mybatis.spring.mapper.MapperFactoryBean">

<!-- queries are defined via annotations in the mapper interface defined here: -->
<property name="mapperInterface" value="com.mybiz.dao.MyMapper"/>
<property name="sqlSessionFactory" ref="sqlSessionFactory"/>
</bean>

<!-- needed for transactional behaviors in the Spring-driven JUnit test -->
<bean id="transactionManager"
class="org.springframework.jdbc.datasource.DataSourceTransactionManager">
<property name="dataSource" ref="datasource"/>
</bean>

<bean id="dao" class="com.mybiz.dao.MyDAO">
<property name="myMapper" ref="myMapper"/>
</bean>
</beans>

The Java interface simply presents the queries and methods in one place with annotations:


public interface MyMapper {

@Insert("insert into myTable(name) values(#{name,jdbcType=VARCHAR})"
int create(@Param("name") String name);

@Select("select name from myTable where name = #{name}") // yep, trivial but it is a POC
String get(@Param("name") String name);

// note that if you are using interfaces for the returned values from get() that this won't work;
// the attempt by MyBatis to instantiate via no-arg constructor will fail. If using XML configuration
// you can instead use typeAliases and resultType attributes to point to the concrete class, or
// do what's in effect the same thing - use the concrete class as the returned type here in the
// interface (e.g. the interface is MyCustomInterface with concrete MyConcreteClass - use the
// latter here as returned value from get())
}

The DAO no longer needs to worry about getting the session, committing it, closing it or catching exceptions (as was needed with iBatis 2.x) - the Mapper Factory Bean wrapper does all of that for you:


public class MyDAO {

private ConfigurationMapper configMapper;

public void setConfigMapper(ConfigurationMapper configMapper) {
this.configMapper = configMapper;
}

public int create(String name) {

return configMapper.create(name);
}

public Configuration get(String name) {

return configMapper.get(name);
}
}

Finally, a JUnit test that demonstrates auto-rollback:


@RunWith(SpringJUnit4ClassRunner.class)
@ContextConfiguration(locations = "my-test-context.xml") // the Spring context file
@TransactionConfiguration(transactionManager = "transactionManager", defaultRollback = true)
@Transactional
public class MyDAOTest {

@Autowired
private MyDAO dao;

@Test
@Rollback(true)
public void create() throws Exception {
final String expected = "foobar";
int numcreated = dao.create(expected);
assert numcreated == 1;
String actual = dao.get(expected);
assert actual != null;
assert actual.equals(expected);
}
}

Run the test and examine your database - the entered row should not be present; convince yourself by setting @Rollback to false for the create method, run it again, and re-examine the database - the row should now be present.

No comments:

Post a Comment