Friday, May 18, 2007

Ant-based Ibatis config and map generation

First I have to say we love ibatis coupled with Spring. Great tools! The purpose of this post however is to show a tool we created for automatic generation of ibatis mapconfig and map files using ant. This is similar in concept to the ibatis 3 idea of annotations and hopefully we will be able to use that when it becomes available. In the mean time, here is what we have.

Overview

  • We use javadoc-like comments within the spring dao to define namespaces, resultmap and queries (select/insert/delete).
  • We use ant to read the java source and generate sqlmap files for each dao class.
  • We use ant to create an sqlmapconfig file with all the sqlmap files generated in the previous step.

Example

Here is a very simple spring-based dao:

package sample.mapgen.dao.ibatis;

import org.springframework.orm.ibatis.support.SqlMapClientDaoSupport;

import sample.mapgen.dom.User;

/**
* Implements the sequence dao using ibatis against postgres
*
* @ibatis.namespace namespace="User"
* @ibatis.alias alias="user" type="sample.mapgen.dom.User"
*
* @ibatis.resultmap id="usermap" class="user"
* property="userId" column="userid"
* property="userName" column="username"
* property="name" column="name"
*
*/
public class SimpleDaoImpl extends SqlMapClientDaoSupport
{
/**
* get user by id
*/
public User getById(int id)
{
/**
* @ibatis.map id="getById" parameterClass="int" resultMap="usermap"
* @ibatis.mapsql
* select userid, username, name from users where userid = #value#
*/
return (User) getSqlMapClientTemplate().queryForObject("User.getById", Integer.valueOf(id));
}

}


All the javadoc-like comments that begin with @ibatis will be processed and turned into xml files. I say javadoc-like comments because javadoc technically does not allow comments within the code body, but I wanted the queries next to the code that uses them for easier readability.

Notice the use of the namespace at the beggining and also within the queryForObject() call. The general idea is simple for the query: the query begins where the @ibatis.mapsql and ends where the next @ibatis tag starts or the comment ends.

After we run ant, we end up with the following files:
  • ibatisSqlMapConfig.xml
    <?xml version="1.0" encoding="UTF-8"?>
    <!DOCTYPE sqlMapConfig PUBLIC "-//iBATIS.com//DTD SQL Map Config 2.0//EN" "http://www.ibatis.com/dtd/sql-map-config-2.dtd">

    <!-- AUTOGENERATED by IbatisMapMaker Ant Task DO NOT CHANGE MANUALLY -->

    <sqlMapConfig>
    <settings
    useStatementNamespaces="true"
    enhancementEnabled="true"
    lazyLoadingEnabled="true"
    />

    <sqlMap resource="sms/util/dao/ibatis/SimpleDaoImpl.ibatismap.xml"/>
    </sqlMapConfig>

  • sample/mapgen/dao/ibatis/SimpleDaoImpl.ibatismap.xml

    <?xml version="1.0" encoding="UTF-8"?>
    <!DOCTYPE sqlMap PUBLIC "-//iBATIS.com//DTD SQL Map 2.0//EN" "http://www.ibatis.com/dtd/sql-map-2.dtd">

    <!-- AUTOGENERATED by IbatisMapMaker Ant Task DO NOT CHANGE MANUALLY -->

    <sqlMap namespace="User">
    <typeAlias type="sample.mapgen.dom.User" alias="user"/>
    <resultMap id="usermap" class="user" >
    <result property="userId" column="userid" />
    <result property="userName" column="username" />
    <result property="name" column="name" />
    </resultMap>
    <select id="getById" parameterClass="int" resultMap="usermap"><![CDATA[
    select userid, username, name from users where userid = #value#
    ]]></select>
    </sqlMap>

Anytime the java file is changed, we just run the ant build and the files are regenerated.

Ant Task

The ant build file contains the following task definition:

      <!-- ==================== Ibatis mapping file =================================== -->
<target name="ibatismapgen" depends="compile" description="Find all ibatis map files and add them to the map config file">
<taskdef name="mapmaker" classname="devtools.ant.ibatis.IbatisMapMakerTask" classpath="c:/cygwin/home/luisl/lib/psanttasks.jar" />
<mapmaker srcdir="${src.home}"
destdir="${build.web}/classes"
mapconfigfile="ibatisSqlMapConfig.xml"
useStatementNamespaces="true"
lazyLoadingEnabled="true"
enhancementEnabled="true"
insertconfigfile="${web.home}/WEB-INF/ibatisTypeHandlers.xml"
>
<fileset dir="${src.home}">
<!-- Limit to only files that contain an ibatis javadoc-like tag -->
<contains text="@ibatis" casesensitive="yes"/>
<include name="**/ibatis/*.java"/>
</fileset>

</mapmaker>
</target>
The mapmaker task contains the following attributes
  • srcdir: this is the directory that represents the base dir for all source java classes
  • destdir: this is the destination directory for all the generated xml files. The files are generated with the same name and path as the source java file but with the extension, ibatismap.xml
  • mapconfigfile: the name and location of the mapconfig file which will include an import of all the map files.
  • useStatementNamespaces/lazyLoadingEnabled/enhancementEnabled: ibatis config file properties
  • insertconfigfile: this will take the given file and insert it into the config file before the sqlmap "import" statements. We use this for our TypeHandlers.

More Features

Includes
The processing of the files adds a feature similar to the ibatis built-in sql include. Basically you can define an include section in any file and include it in any other file. We use this for defining columns used in select statements. For example, we could define an include in the SimpleDaoImpl class above as follows:

/**
* @ibatis.include id="usercols"
* @ibatis.mapsql
* u.userid,
* u.username,
* u.name
*/

and then in another part of the dao reference this include as:

/**
* @ibatis.map id="getById" parameterClass="int" resultMap="usermap"
* @ibatis.mapsql
* select <include id="usercols"/> from users where userid = #value#
*/

We could also reference the include from another completely different file, such as EmployeeDaoImpl using the namespace of the source dao, as follows:

/**
* @ibatis.map id="getEmpUsers" parameterClass="int" resultMap="User.usermap"
* @ibatis.mapsql
* select <include id="User.usercols"/> from emp e, users u where u.userid = e.userid
*/

We have found this very valuable for maintainability since the definition for a table/object relationship is centralized in one location.

We are working on making result maps also shareable between daos. This will mean splitting the result map section and the query definition section of the map file and listing all the result maps first in the config file.

XML in Query
By default, the xml for all queries is created within a <![CDATA[ ]]> tag to facilitate having less than or greater than signs in the query. However, when we want to leverage ibatis' dyname query generation, we need xml in the query. To turn CDATA off, simply add useCDATA="no" to the ibatis.map definition. For example:

@ibatis.map id="getUsersGroup" parameterClass="map" resultMap="usermapgroup" useCDATA="no"


Conclusion

We have found this tool very helpful in working with ibatis and hope that it is of value to others as well. We would love to get your feedback on how to improve it.