Mapping Java Objects to the results of SQL Queries

As of December 2006, iBATIS has been simplified to perform one function, and only one function: data mapping. While iBATIS used to support a DAO layer, this has been deprecated in favour of using Spring and IoC. This article will stick to discussing the data mapper; for more information on the DAO implementation, please refer to the Spring framework documentation (specifically here). The key documentation for iBATIS is the Developer Guide, with the very latest here in the SVN repository.

OK, on to Hibernate vs. iBATIS...

  • While they've both involved with having your application persist & retrieve data, comparing them is an "apples vs. oranges" comparison, in that while they both do the job, they're significantly different in certain ways.

Fundamentally, iBATIS maps Java Objects to the results of SQL Queries, whereas Hibernate maps Java Objects directly to database tables, traditional Object-Relational Mapping. The benefits of Hibernate are that it automatically generates all the SQL for your and the cache invalidation can be more fine grained. iBATIS is more flexible especially if you are a strong SQL query writer. You have control over exactly how the SQL queries are written.

Another way of looking at it to say that Hibernate works well if you have mostly standard queries(CRUD, Find by Criteria, etc.) and if you are designing your object model first. If you are working with a legacy system or a schema designed by a DBA, iBATIS often makes a better choice. Of course, we're not discussing absolutes here, they're both much better than raw SQL!

The thing with iBATIS is that there isn't a whole lot of magic, and you get full control over the SQL, so you can see what's going on and how to change it. Compared to other frameworks, it has a shorter learning curve, but can take more time to develop and maintain, since you have to write all your queries (using Abator can short-circuit this step) and if your object model changes you have to go through all your queries and make sure to make all the necessary changes to reflect the changes in your object model.

Introduction

While there's nothing particularly special about the use of iBATIS within a Wicket application, the normal Wicket requirement of ensuring that things that you store in your pages are Serializable mean that you may need to be careful how you store references to DAO-type objects.

The following is intended to suggest one way in which this might be done.
Note: While this example uses Spring and the SpringBean annotation, neither are strictly required, but they do simplify things enough that I felt it was worth using them, even without any previous knowledge of them.

Overview

Dramatis Personae

Another take on the above may be obtained by using the Abator code generator tool - See below.

Elements

ItemDao

An interface describing the DAO operations we want, e.g. load, save, delete, etc.

public interface EntryDao  {
    Entry load(String name);
    void save(Entry  config);
    void delete(String name);
}

EntryDaoImpl

An implementation of the DAO.
the particular schema this is accessing is a simple Name/Value/LastChanged one, i.e. No distinct id

public class EntryDaoImpl extends SqlMapClientDaoSupport implements EntryDao, Serializable {
    public Entry load(String name) {
        return (Entry) getSqlMapClientTemplate().queryForObject("Entry.get", name);
    }

    public void save(Entry entry) {
        if (entry.getLastChanged() == null) {
            getSqlMapClientTemplate().insert("Entry.insert", entry);
        } else {
            getSqlMapClientTemplate().update("Entry.update", entry);
        }
    }

    public void delete(String name) {
        getSqlMapClientTemplate().delete("Entry.delete", name);
    }
}

Entry.map.xml

IBATIS SqlMap definitions, one per operation.

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

<sqlMap namespace="Entry">

    <select id="get" parameterClass="string" resultClass="package.name.model.Entry">
	SELECT name, value, LAST_CHANGE as lastChange FROM TB_ENTRY WHERE name = #value#
    </select>

    <!-- Use Entry object (JavaBean) properties as parameters. Each of the
        parameters in the #hash# symbols is a JavaBeans property. -->
    <insert id="insert" parameterClass="package.name.model.Entry">
        INSERT INTO TB_CONFIG (NAME, VALUE, DESCRIPTION, LAST_CHANGE) VALUES (#name#, #value#, #description#, SYSDATE)
    </insert>

    <update id="update" parameterClass="package.name.model.Entry">
        UPDATE TB_ENTRY
            SET
                VALUE = #value#,
                DESCRIPTION = #description#,
                LAST_CHANGE = SYSDATE
            WHERE NAME = #name#
    </update>

    <!-- Use primitive properties as parameter for delete. -->
    <delete id="delete" parameterClass="string">
        DELETE FROM TB_ENTRY WHERE NAME = #value#
    </delete>
</sqlMap>

A couple of notes:

  1. I'm using the Oracle SYSDATE keyword, but I could have made if DB-neutral if needed. However, as the app this example is derived from will only be deployed on Oracle, I didn't!
  2. The #value# in the delete block is just a placemarker. The actual name, i.e. "value", isn't significant there, unlike the names used in the update/insert sections where they're used to determine the JavaBean accessors to use.

Spring Configuration

Typical Wicket Spring Initialisation

<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE beans PUBLIC "-//SPRING//DTD BEAN//EN" "http://www.springframework.org/dtd/spring-beans.dtd">

<beans>
    <!-- 
        Allow the use of properties from application.properties 
	throughout this configuration file
    -->
    <bean id="placeholderConfig"
          class="org.springframework.beans.factory.config.PropertyPlaceholderConfigurer">
        <property name="location"><value>classpath:application.properties</value></property>
        <property name="systemPropertiesModeName"><value>SYSTEM_PROPERTIES_MODE_OVERRIDE</value></property>
    </bean>

    <!--data source definition-->
    <bean id="dataSource" class="org.apache.commons.dbcp.BasicDataSource">
        <property name="driverClassName"><value>${jdbc.driver}</value></property>
        <property name="url"><value>${jdbc.url}</value></property>
        <property name="username"><value>${jdbc.user}</value></property>
        <property name="password"><value>${jdbc.password}</value></property>
    </bean>

    <!--
        <bean id="dataSource" class="org.springframework.jndi.JndiObjectFactoryBean">
            <property name="jndiName"><value>${jndi.name}</value></property>
        </bean>
    -->

IBATIS-specific

    <bean id="sqlMapClient" class="org.springframework.orm.ibatis.SqlMapClientFactoryBean">
        <property name="configLocation" value="classpath:sqlMapConfig.xml"/>
        <property name="dataSource" ref="dataSource"/>
    </bean>

    <!-- setup dao implemations -->
    <bean id="entryDaoImpl" class="package.impl.EntryDaoImpl">
        <property name="sqlMapClient" ref="sqlMapClient"/>
    </bean>

    <!-- setup transaction manager	-->
    <bean id="transactionManager" class="org.springframework.jdbc.datasource.DataSourceTransactionManager">
        <property name="dataSource" ref="dataSource"/>
    </bean>

    <!-- these are transactional proxys for dao which ensure proper transaction handling -->
    <!-- Note that they're loaded in via @SpringBean calls. -->
    <bean id="baseTransactionProxy"
          class="org.springframework.transaction.interceptor.TransactionProxyFactoryBean"
          abstract="true">
        <property name="transactionManager" ref="transactionManager" />
        <property name="transactionAttributes">
            <props>
                <prop key="save">PROPAGATION_REQUIRED</prop>
                <prop key="delete">PROPAGATION_REQUIRED</prop>
                <prop key="*">PROPAGATION_REQUIRED,readOnly</prop>
            </props>
        </property>
    </bean>

    <bean id="entryDao" parent="baseTransactionProxy">
        <property name="target" ref="entryDaoImpl" />
    </bean>
    <!-- End transaction management -->

Typical Wicket Spring Application

Just for completeness...

    <!-- Wicket Application -->
    <bean id="wicketApplication" class="com.zaryba.topup.web.config.TopUpConfigApplication"/>
</beans>

iBATIS' sqlMapConfig.xml

This contains the 'global' settings for iBATIS and the collection of SQL maps in use. When using together with Spring, it can be very concise...

<!DOCTYPE sqlMapConfig
        PUBLIC "-//ibatis.apache.org//DTD SQL Map Config 2.0//EN"
        "http://ibatis.apache.org/dtd/sql-map-config-2.dtd">

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

    <!-- Identify all SQL Map XML files to be loaded by this SQL map.
         Notice the paths are relative to the classpath. For now, we only have one -->
    <sqlMap resource="sqlmap/Entry.map.xml" />
</sqlMapConfig>

DAO usage

Note that the Spring TransactionProxyFactoryBean is serializable, so we can do the following in the Wicket component as long as our application extends AnnotSpringWebApplication.

public class MyPage extends WebPage {
    @SpringBean(name="entryDao")  private EntryDao entryDao;

    public MyPage() {
        ...

Note: If we don't want to/can't use @SpringBean then there are other options available, e.g. [ storing the DAO in the Application|Spring#Application_Object_Approach ], etc.

Misc Notes

Paging

iBATIS SqlMapClientTemplate does have a queryForList(String statementName, Object parameterObject, int skipResults, int maxResults) method, but note that it works by obtaining all the elements(keys) from the DB then just returning the selected part of the list. If wanting to handle this more efficiently, it's down to the (DB-specific) SQL.

Oracle paging

Oracle's paging is rather involved due to it's particular implementation of ROWNUM. However, this example shows how it can work. (It also shows a bit more too, e.g. dynamic & sortable SQL, but that's not the main point here!)

public EntryDataProvider extends SortableDataProvider {
    ...
    public Iterator iterator(int first, int count) {
        QueryParam qp = null;
        SortParam sp = getSort();
        qp = new QueryParam(first, count, sp.getProperty(), sp.isAscending());
        return entryDao.find(qp, filter);
    }
    ...
}

In the DaoImpl, set up a 'map' data structure to pass various things in.

    public Iterator find(QueryParam qp, Entry filter) {
        Map map = new HashMap();
        String name = filter.getName();
        map.put("name", name == null ? null : "%" + name.toUpperCase() + "%");
        map.put("sort", qp.getSort());
        map.put("sortasc", (qp.isSortAsc() ? " asc" : " desc"));

        int first = qp.getFirst();
        map.put("min", first + 1);
        map.put("max", first + qp.getCount());

        return getSqlMapClientTemplate().queryForList("Config.getList", map).listIterator();
    }

In the Entry.sql.map, the variables are taken from the Map passed in.

    <select id="getList" parameterClass="map" resultClass="package.model.Entry">
		SELECT * FROM (SELECT a.*, ROWNUM rn FROM (SELECT name FROM TB_ENTRY
        <dynamic prepend="WHERE">
            <isNotNull prepend="AND" property="name">
                UPPER(name) LIKE #name#
            </isNotNull>
        </dynamic>
        ORDER BY $sort$ $sortasc$) a WHERE ROWNUM <![CDATA[ <= ]]> #max#) WHERE rn <![CDATA[ >= ]]> #min#
    </select>

Some points...

Abator - Code Generator for iBATIS (Recommended!)

See http://ibatis.apache.org/docs/tools/abator/ for documentation.

Abator is a code generator for iBATIS that will examine a database table/tables and generate iBATIS artefacts that can be used to access the table(s).

Abator seeks to make a major impact on the large percentage of database operations that are simple CRUD (Create, Retrieve, Update, Delete). You will still need to hand code SQL and objects for custom queries, or stored procedures, but it generates code that should cover much of the required usage (See Using the Abator Generated Objects) and provide a good basis for creating the rest.

Abator will generate:

Abator can run as a standalone JAR file, or as an Ant task, or as an Eclipse plugin.

I'd really recommend starting with this, as it proves a good jump-start!