![]() | What's iBATIS, or iBATIS vs. Hibernate 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 OK, on to Hibernate vs. iBATIS...
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
- EntryDao : An interface describing the DAO operations we want, e.g. count, find, load, save, delete
- EntryDaoImpl : An implementation of the DAO. For Spring/IBATIS, this will extend SqlMapClientDaoSupport and make the DB call using a definition in an SqlMap relating to the Item.
- Entry.map.xml : IBATIS SqlMap definitions, one per operation.
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. [Note: 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:
- 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!
- 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...
- The dynamic section is more complex than needed for the example, as it caters for multiple sub-elements. The idea is that you have multiple blocks and the first time one triggers, the prepend from the outer dynamic block overrides the prepend from the inner block.
- If using XML 'reserved' chars, e.g. < or >, you'll need to escape them with CDATA sections.
- The variables are used in 3 different ways in this example
- The 'normal' #name# method
- As a test, e.g. <isNotNull property="name" ...
- To create dynamic SQL, e.g. ORDER BY $sort$
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:
- SqlMap XML Files
- Java Classes to match the primary key and fields of the table(s)
- Abator Generated Java Model Classes
- Java 'Example' classes to help with selecting subsets of the tables(s)
- DAO Interfaces & DAOImpl Classes that use the above objects. Various formats (e.g. IBATIS, SPRING, etc)
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!