http://www.developer.com/

Back to article

Using iBatis SQL Maps for Java Data Access


April 28, 2004

Finding the best approach when accessing a database from Java can be a daunting task. The most common solution is to program directly to the JDBC (Java Database Connectivity) APIs. The result is hard-to-read source files, bloated with complex code that has nothing to do with business logic. To make matters worse, JDBC does nothing to address the natural differences that usually exist between a system's object model and its relational data model. This necessitates still more code to glue these disparate views together. There is clearly a need for tools to assist us in this area.

The iBATIS Database Layer, written by Clinton Begin, is a collection of open-source components that assist Java developers with data access. The most important of these components is the SQL Maps framework. SQL Maps provides an efficient way to graph database values to Java objects through the use of XML configuration files. This article will use the latest production release of SQL Maps, 1.3.1, to demonstrate how SQL Maps work using a series of examples. It will then conclude with a discussion of the benefits of using iBatis SQL Maps.

A First Look

To view SQL Maps in action, I will use a simple demo application that allows employees to make conference room reservations. It will not be possible to cover SQL Maps in its entirety, but hopefully there will be enough here to entice you to visit the iBATIS site and explore the project in greater detail.

There are two tables to consider in our application, conference_rooms and room_reservations. The conference_rooms table identifies conference_rooms by id and name. The field number_of_seats can be used to guage the size of a room. The room_reservations table, linked to conference_rooms by room_id, stores reservation times and the team ('development team', 'accounting team') making the reservation. For simplicity's sake, there is a one-to-one mapping between these tables, conference_rooms and room_reservations, and the JavaBeans ConferenceRoom and RoomReservation.

Given this setup, let's first consider a code snippet that you might use if populating a ConferenceRoom bean with JDBC:

String roomToUse        = "Conf Room North";ConferenceRoom confRoom = new ConferenceRoom();try{  Connection conn = dSrc.getConnection();  PreparedStatement pstmt =    conn.prepareStatement("SELECT room_id,room_name,number_of_seats,                           room_active " +     "FROM conference_rooms WHERE room_name = ?";  pstmt.setString(1,roomToUse);  rstst = ps.executeQuery();  rstst.next();  confRoom.roomId        = rstst.getInt(1);  confRoom.roomName      = rstst.getString(2);  confRoom.numberOfSeats = rstst.getInt(3);  confRoom.roomActive    = rstst.getBoolean(4);  rstst.close();  pstmt.close();}catch(SQLException e) {   throw newRuntimeException(e);}finally {  conn.close();}

As simple as the code's objective may be, it is complicated by JDBC 'plumbing.' Let's try this once again, the SQL Maps way:

String roomToUse = "Conf Room North";ConferenceRoom confRoom = (ConferenceRoom)    sqlMap.executeQueryForObject("getConferenceRoomByName", roomToUse);

The SqlMap method, getQueryForObject, accepts two String parameters. The first parameter identifies the name of the mapped SQL statement we use to access the database. The second specifies a room name used in the query. The bean is instantiated, populated, and then returned, leaving us to focus our coding efforts on application logic.

Now, I want to step backwards through this example and examine what makes such clean code possible. To do this, we need to look at two additional items. The first is the SQL Map file. This file is an XML descriptor that contains our mapped SQL statements. At runtime, these statements are read into an application. When "getConferenceRoomByName" is referenced in Java, a PreparedStatement object executes the query and a ResultSet populates the bean. The demo application uses a single SQL Map file called Conferences.xml. An application may use several such SQL Map files.

<!Conferences.xml SQL Map file--><?xml version="1.0" encoding="UTF-8"?><!DOCTYPE sql-map PUBLIC "-//iBATIS.com//DTD SQL Map 1.0//EN"   "http://www.ibatis.com/dtd/sql-map.dtd"><sql-map name="Conferences">  <mapped-statement name="getConferenceRoomByName"                    result-class="entities.ConferenceRoom">    SELECT room_id         as roomId,           room_name       as roomName,            number_of_seats as numberOfSeats,           room_active     as roomActive      FROM conference_rooms      WHERE room_name = #value#  </mapped-statement></sql-map>

The bulk of the "getConferenceRoomByName" mapped statement consists of SQL. The result-class attribute specifies the fully qualified Java object returned from a mapped statement. The #value# keyword denotes that a statement's parameter is either a String or a Java primitive, such as int.

The final piece of the puzzle is the SQL Map Configuration file:

<!SqlMapConfig.xml SQL Map configuration file--><?xml version="1.0" encoding="UTF-8"?><!DOCTYPE sql-map-config PUBLIC "-//iBATIS.com                                  //DTD SQL Map Config 1.0//EN"   "http://www.ibatis.com/dtd/sql-map-config.dtd">   <sql-map-config>  <datasource name = "hsql"     factory-class="com.ibatis.db.sqlmap.datasource.SimpleDataSourceFactory"            default="true">    <property name="JDBC.Driver" value="org.hsqldb.jdbcDriver"/>    <property name="JDBC.ConnectionURL"              value="jdbc:hsqldb:hsql://localhost/conferences"/>    <property name="JDBC.Username" value="sa"/>    <property name="JDBC.Password" value=""/>   </datasource>  <sql-map resource="maps/Conferences.xml"/> </sql-map-config>

The purpose of this file is two-fold. First, it configures a JDBC DataSource. In this case, it uses iBATIS' own DataSource implementation, SimpleDataSource. Secondly, the file declares each SQL Map file that an application uses. Our examples use the hsqldb database and a single Map file, Conferences.xml.

Upon application startup, we search the classpath for this configuration file and process it.

String resource = "maps/SqlMapConfig.xml";Reader reader   = Resources.getResourceAsReader(resource);SqlMap sqlMap   = XmlSqlMapBuilder.buildSqlMap(reader);

The configuration needs to happen but one time, prior to the calling of any mapped statements. The sqlMap instance could be a Singleton, or possibly a ServletContext object in a web application, that we retrieve when we need to call a mapped statement. The Resource class we are using is provided by iBATIS to read files from the classpath.

SQL Maps in Action

Now that we have some insight into how SQL Maps works, what follows is a series of code excerpts that highlight additional features of SQL Maps. In each case, a set of parentheses identifies the relevant file.

A reservation system wouldn't be much without a means to create reservations. In this example, a reservation is added for the confRoom bean.

(SQL MAP)<mapped-statement name="insertReservation">  INSERT INTO room_reservations    (room_id,reservation_start,reservation_end,reservation_team)  VALUES    (#roomId#,#reservationStart#,#reservationEnd#,#reservationTeam#)</mapped-statement>(JAVA)Calendar start = new GregorianCalendar();start.set(2004,Calendar.MARCH,10,12,30);Calendar end   = new GregorianCalendar();end.set(2004,Calendar.MARCH,10,14,30);RoomReservation ourReservation = new RoomReservation();ourReservation.setRoomId(confRoom.getRoomId());ourReservation.setReservationStart(new Date(start.getTimeInMillis()));ourReservation.setReservationEnd(new Date(end.getTimeInMillis()));ourReservation.setReservationTeam("Development Team");sqlMap.executeUpdate("insertReservation", ourReservation);

A JavaBean is passed to this SQL Maps statement. The bean's properties are referenced with the syntax #{property}#. The executeUpdate method is used to insert, update, or delete from a database.

There should now be a single reservation for confRoom. Passing in roomId, which happens to be a Java primitive of type int, we obtain confRoom's reservation count.

(SQL MAP)<mapped-statement name="getReservationCountForRoom"                  result-class="java.lang.Integer">  SELECT COUNT(1) as value    FROM room_reservations    WHERE room_id = #value#</mapped-statement>(JAVA)Integer cntReservations = (Integer) sqlMap.executeQueryForObject(  "getReservationCountForRoom", new Integer(confRoom.getRoomId()));System.out.println("Reservation Cnt for " + confRoom.getRoomName(                                          + " is " + cntReservations);

We've already seen the #value# syntax. As you recall, it is used when working with Java primitives. SQL Maps works solely with Java objects so primitives, either passed in or returned from a statement, must be wrapped in their object equivalents. The clause 'as value' is used to select a single, primitive value from the database.

Along with mapped-statement, a SQL Map file may contain the parameter-map and result-map attributes for handling more complex mapping requirements. Earlier, when we persisted a RoomReservation bean to the database, we used an implicit parameter map. SQL Maps used Java Reflection to match each #{property}# entry in the mapped statement with a bean property.

Using an explicit parameter map provides two useful capabilities. First, it allows you to specify a database type for an object or bean property. Second, an explicit parameter map gives you a chance to provide a default value when a parameter is null. Explicit parameter maps can be defined with <parameter-map> </parameter-map> tags, and then referenced in a mapped statement with the "parameter-map" attribute. However, given the limited goals of a parameter map, SQL Maps provides an alternative, 'inline' syntax to simplify things.

To obtain the number of reservations for a particular team while specifying a parameter's database type and providing a null default, use the following:

(SQL MAP)  <!--Get count using an inline parameter map, passing in a String.-- >  <mapped-statement name="getReservationCountByTeam"      parameter-class="java.lang.String"      result-class="java.lang.Integer">     SELECT COUNT(1) as value       FROM room_reservations       WHERE reservation_team = #value:VARCHAR:NONE#;  </mapped-statement>    (JAVA)  //Use an in-line parameter map.  Integer cntTeamReservations = (Integer) sqlMap.executeQueryForObject(    "getReservationCountByTeam", ourReservation.getReservationTeam());

Direct your attention to the WHERE clause. It states that the parameter identifying the team must be compatible with the database type VARCHAR. In addition, if this parameter object happens to be null, 'NONE' is used.

It is far more common to find an explicit result map in a SQL Map file. Explicit result maps allow you to specify a default return value when a query returns null. More importantly, result maps enable a developer to map complex objects into an application.

Previously, mapped statements returned either primitive wrappers or entire JavaBeans. Consider this next example where we want to return a subset of properties for the conference room with the most number of seats.

(SQL MAP)<result-map name="get-id-name-and-number-of-seats"            class="java.util.HashMap">   <property name="id" column="room_id"/>   <property name="name" column="room_name" null="NO NAME"/>   <property name="seatNum" column="number_of_seats"/></result-map><mapped-statement name="getLargestConferenceRoomInfo"                  result-map="get-id-name-and-number-of-seats" result-class="entities.ConferenceRoom">  SELECT *      FROM conference_rooms    WHERE number_of_seats = (SELECT MAX(number_of_seats)    FROM conference_rooms);</mapped-statement>(JAVA)Map largestRoomInfo = new HashMap();largestRoomInfo = (HashMap) sqlMap.executeQueryForObject(      "getLargestConferenceRoomInfo",null);System.out.println("largest room info: id : "  + largestRoomInfo.get("id")  + ",name : "  + largestRoomInfo.get("name")  + ",seatNum : "  + largestRoomInfo.get("seatNum"));

The Map largestRoomInfo returns with map keys for each bean property name. The ability to map results to a Java Collection gives us a good deal of flexibility and power. Also note that this result map is not bound to only one mapped statement. Once defined, a result map can be reused again and again.

The final two examples involve a scenario almost every Java applications will encounter. Objects do not exist in their own isolated bubble. Rather, objects have relationships with other objects and these relationships determine behavior. The room_reservations table is linked to conference_room by the room_id column. In the world of Java, however, this might manifest itself in a java.util.List attribute on the ConferenceRoom bean. This List would contain all associated RoomReservation beans. We will modify the ConferenceRoom JavaBean by adding the reservationsList property. Now, if we wish to populate a ConferenceRoom bean, we will want its List of RoomReservations too.

(SQL MAP)  <result-map name="get-conference-room"              class="entities.ConferenceRoom">     <property name="roomId" column="room_id"/>     <property name="roomName" column="room_name"/>     <property name="numberOfSeats" column="number_of_seats"/>     <property name="roomActive" column="room_active"/>     <property name="reservationsList" column="room_id"               mapped-statement="getReservationsByRoomId"/>  </result-map>    <result-map name="get-room-reservation"              class="entities.RoomReservation">     <property name="reservationId" column="reservation_id"/>     <property name="roomId" column="room_id"/>     <property name="reservationStart" column="reservation_start"/>     <property name="reservationEnd" column="reservation_end"/>     <property name="reservationTeam" column="reservation_team"/>  </result-map>    <mapped-statement name="getConferenceRoomById"                    result-map="get-conference-room">    SELECT *     FROM conference_rooms    WHERE room_id = #value#;   </mapped-statement>       <mapped-statement name="getReservationsByRoomId"                     result-map="get-room-reservation">    SELECT *     FROM room_reservations     WHERE room_id = #value#;   </mapped-statement>(JAVA)ConferenceRoom roomWithReservations =     (ConferenceRoom) sqlMap.executeQueryForObject(       "getConferenceRoomById", new Integer(confRoom.getRoomId()));

Okay, so this one is a little more complex. But there really is not that much to it. Two result maps are used, one for ConferenceRoom and one for RoomReservations. Two mapped statements populate the beans by referencing those result maps. Obtaining the reservations is made possible by the following line in the get-conference-room result map:

<property name="reservationsList" column="room_id"          mapped-statement="getReservationsByRoomId"/>

SQL Maps looks to the ConferenceRoom JavaBean for a List by the name of reservationsList. If the List property is there, SQL Maps uses the getReservationsByRoomId statement to populate it.

The SqlMap class also contains some convenience methods that return Java Collections. To retrieve a List of RoomReservations, without the ConferenceRoom bean, we can do this:

(SQL MAP)<mapped-statement name="getReservationsList"                  result-map="get-room-reservation">  SELECT *     FROM room_reservations; </mapped-statement>    (JAVA)List reservationsList =  sqlMap.executeQueryForList("getReservationsList", null);

I've demonstrated a number of ways to use SQL Maps, yet I close this section with many great SQL Map features unexamined. These features include transaction support, object caching, support for dynamic querying, and the ability to extend mapped statements for maximum code reusability.

The Many Benefits of SQL Maps

If I've done my job at all, you have a fairly good idea how using SQL Maps can simplify Java data access. However, it may not be amiss at this point to list a few important benefits the SQL Maps framework provides:

  • Eliminates low-level JDBC code that is hard to read and prone to bugs
  • Provides an efficient way to map database data, once retrieved, to Java objects
  • Allows modifications to SQL configuratively, without recompiling code
  • Enables an application to switch databases, again without recompilation
  • Lets DBAs isolate and test SQL code for efficiency

With SQL Maps you can accomplish almost everything you could using JDBC, while sparing yourself the code complexity that JDBC inevitabely brings with it.

Conclusion

This article has demonstrated the basic features of SQL Maps. If you wish to work with the examples shown in this article, you can download them here. One of the best things about the iBATIS SQL Maps framework is that it is not only easy to use, but also very well documented. I encourage you to visit the iBATIS site and read the Developer's Guide to learn more.

About the Author

Michael Klaene is a Senior Consultant with Sogeti LLC. He has spent over 9 years in Information Technology and is an experienced Systems Analyst, delivering solutions that involve numerous technologies, such as J2EE and .NET.

Sitemap | Contact Us

Thanks for your registration, follow us on our social networks to keep up-to-date