Thursday, July 9, 2009

Spring 3.0 Cheatsheet: Integration with iBatis

This is the next in a series of tips, insights and recommendations around Spring 3.0. This time, I offer our experiences to-date around use of Spring's ORM facility, or more accurately its integration with iBatis (which is not, strictly speaking, an OR mapper).

iBatis - General
  • Use iBatis "implicit result mapping", i.e. adding an alias to each column in the select field list, to match up with Java object properties so iBatis will automatically map them for you (and as such, there's no need for resultMap constructs). For example, the following maps database column names (that might need to follow database naming conventions) to the property names of the corresponding Java object:
<sql id="allFieldsFromWebapp">
      APP_ID as appID,
      DISPLAY_NAME as displayName,
      DESCRIPTION as description,
      URL as url
    from WEBAPP
</sql>

Now, instead of referring to resultMap id's, reference the class to be populated:

<typeAlias alias="WebApp" type="com.mybiz.model.WebApp"/>
....
<select id="selectAll" resultClass="WebApp">
    select  <include refid="allFieldsFromWebapp"/>
</select>
<select id="selectById" parameterClass="String"  resultClass="WebApp">
    select <include refid="allFieldsFromWebapp"/>
    where APPID = #id#
</select>

Using the <sql>, <include> and <typeAlias> tags, we gain opportunity for reuse in the SQL map file.
  • Add JDBC type as suffix to placeholder parameters in iBatis queries, as needed:

UPDATE person SET
title = #title#,
given_names = #givenNames#,
last_name = #lastName#,
date_of_birth = #dateOfBirth:DATE#
WHERE id = #id#

In the above statement, #dateOfBirth:DATE# specifies day, month and year only - since, in this case, there's no need for hours, minutes and seconds.

  • To monitor iBatis-generated SQL queries, add log4j.logger.java.sql=DEBUG to your log4j configuration file; however, this shows the precompiled statements only, without parameter substitution.
iBatis-Spring: General Database
  • Factor connection details out of the iBatis SQL map configuration and into Spring beans configuration instead, to simplify the iBatis configuration (since these details often vary between environments (eg development, test, production)) and to consolidate application-wide configuration information into a single place. Put these connection details into e.g. an Apache DBCP DataSource bean. Use the Spring PropertyPlaceholderConfigurer bean to support the externalization.
In the Spring beans file:

    <bean class="org.springframework.beans.factory.config.PropertyPlaceholderConfigurer">
        <property name="location" value="classpath:database.properties"/>
    </bean>

    <bean id="dbcp-dataSource" class="org.apache.commons.dbcp.BasicDataSource" destroy-method="close">
        <property name="driverClassName" value="${database.driver}"/>
        <property name="url" value="${database.url}"/>
        <property name="username" value="${database.user}"/>
        <property name="password" value="${database.password}"/>
    </bean>

In database.properties, which exists in the default package of the classpath as per the above specification:


database.driver=oracle.jdbc.OracleDriver
database.url=jdbc:oracle:thin:@my-dev-host.mybiz.com:1521:XE
database.user=system
database.password=foobar


Now the SQL map configuration file is not concerned with connection details; it simply deals with SQL maps:

<sqlMapConfig>
  <sqlMap resource="com/mybiz/persistence/WebApp.xml"/>
</sqlMapConfig>

  • Initialize your DAO using Spring's SQL map client factory, handing it both the location of the iBatis SQL map configuration file and the DBCP data source bean as initialization parameters.
In the DAO, provide for setter-injection of an SQL map client, to be used elsewhere in your code to invoke iBatis queries:

package com.mybiz.persistence;

import com.ibatis.sqlmap.client.SqlMapClient;

public class MyDao implements DaoBase {

    private SqlMapClient sqlMapClient;

    public SqlMapClient getSqlMapClient() {
        return this.sqlMapClient;
    }

    public void setSqlMapClient(SqlMapClient sqlMapClient) {
        this.sqlMapClient = sqlMapClient;
    }

    .....

    public MyObjectCollection getAll() throws SQLException {
        MyObjectCollection coll= new MyObjectCollection ();
        coll.set((List) getSqlMapClient().queryForList("selectAll"));
        return coll;
    }
}

In the Spring beans configuration:

    <bean id="sqlMapClient" class="org.springframework.orm.ibatis.SqlMapClientFactoryBean">
        <property name="configLocation" value="classpath:com/twc/registry/persistence/WebAppDao.xml"/>
        <property name="dataSource" ref="dbcp-dataSource"/>
    </bean>

    <bean id="webapp-dao" class="com.twc.registry.persistence.WebAppDao">
        <property name="sqlMapClient" ref="sqlMapClient"/>
    </bean>


The SQL map client will automagically read in the SQL map configuration information.
Note that with the Spring XML beans file under ./WEB-INF, you must use the "classpath:" prefix to reference the iBatis SQL map configuration file.

Alternately, one can use the Spring SQL map client class instead of the iBatis version (see section 14.5 in the the Spring Reference Doc). The primary advantage here, as far as I can tell, is that formerly checked SQLExceptions are now wrapped in unchecked exceptions. However, I'd consider this optional, and for that matter undesirable in certain situations - e.g., in a RESTful web service where specific checked exceptions are caught so they can be mapped to corresponding HTTP response codes (and in just such a situation, I've chosen the iBatis SQL map client instead of the Spring version). The debate around documenting checked (and unchecked) exceptions as an important part of specifying program behavior is beyond the scope of this post; bottom line, sometimes wrapping exceptions that you can't do anything about with unchecked ones is useful, but sometimes your methods will intentionally throw their own checked exceptions that clients are meant to handle explicitly.

iBatis-Spring: Transactions
  • Use the Spring TransactionManager and annotation-driven transaction management to establish simple, declarative transactional behavior. Use the JDBC data source transaction manager unless you expect to be managing multiple resources in "global" transactions or want the application server to manager transactions (e.g. to take advantage of advanced features like transaction suspension, etc.), in which case use the JTA transaction manager. Downside of JTA transactions is lack of ability to test outside the container (since it requires JNDI and possibly other container functionality).You'll need to provide the AOP autoproxy tag and associated namespace attributes to get declarative transactions to work.
In the Spring beans file - note the transaction manager references the previously established DBCP data source:

<beans xmlns="http://www.springframework.org/schema/beans"
       xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
       xmlns:tx="http://www.springframework.org/schema/tx"
       xmlns:aop="http://www.springframework.org/schema/aop"
       xsi:schemaLocation="
       http://www.springframework.org/schema/tx
       http://www.springframework.org/schema/tx/spring-tx-2.5.xsd
       http://www.springframework.org/schema/aop
       http://www.springframework.org/schema/aop/spring-aop-2.5.xsd
       http://www.springframework.org/schema/beans
       http://www.springframework.org/schema/beans/spring-beans.xsd
       ">
    <bean id="txnMgr" class="org.springframework.jdbc.datasource.DataSourceTransactionManager">
        <property name="dataSource" ref="dbcp-dataSource"/>
    </bean>
    <tx:annotation-driven transaction-manager="txnMgr"/>
    <aop:aspectj-autoproxy/>

  • Annotate DAO methods as appropriate (i.e. non-read-only unless you care about non-repeatable reads, etc.) with @Transactional. This tells Spring that a transaction is required and to start one if it's not already executing within a transactional context (because the default propagation is REQUIRED). That method body will execute as one transactional unit and will auto-commit if it completes successfully. If a runtime exception is thrown, the transaction will be rolled back. This becomes increasingly important as your java method executes more than one operation (multiple updates, delete + insert, etc.)
    @Transactional
    public void delete(String objId) {
        getSqlMapClient().delete("deleteById", objId);
    }
  • Use of Spring AOP means use of proxies; so, if you want to use JDK dynamic proxying instead of CGLIB proxying, your DAOs should implement an interface that specifies its public methods (as implied above with MyDao implements DaoBase). Dynamic proxying is recommended for these reasons: with CGLIB,
    • there are more library dependencies
    • final methods cannot participate in AOP
    • constructors are called twice due to CGLIB implementation details (see the Spring Reference manual, section 9.5.5).
  • While using interfaces supports annotating methods in the interface only - i.e. you don't need to maintain these in the implementation - this is considered risky by the Spring team; so, at a minimum annotate concrete classes, and optionally annotate interfaces also (the former for the functionality, optionally the latter for documentation).
  • How do you know you're getting transactional behavior? You can monitor transaction activity by turning on a log4j appender to debug for TransactionInterceptor; then, add and remove @Transactional to various methods in your interface to observe the logging statements. In a log4j.properties file:

log4j.logger.org.springframework.transaction.interceptor.TransactionInterceptor=debug


  • While transaction behaviors can be configured programmatically, this couples the code to Spring Framework and as such is not preferred.
  • Beware of self-invocation where you expect transactional behavior - unless there's already a transactional context, a new one will not be created. Consider the use of so-called AspectJ mode if you expect self-invocations to be wrapped with transactions as well (set tx:annotation-driven mode attribute to "proxy" - but this will require use of CGLIB libraries).

Resources


Spring and iBatis Tutorial
http://www.cforcoding.com/2009/06/spring-and-ibatis-tutorial.html

iBatis Wiki
http://opensource.atlassian.com/confluence/oss/display/IBATIS/Converting+iBATIS+DAO+to+Spring+DAO

iBatis FAQ
http://opensource.atlassian.com/confluence/oss/display/IBATIS/How+do+I+reuse+SQL-fragments

Spring Reference Manual
http://static.springsource.org/spring/docs/3.0.0.M3/spring-framework-reference/pdf/spring-framework-reference.pdf

No comments:

Post a Comment