Here is an example of how to set up a custom type handler with a complex property. This example uses the enum features of Java 1.5 but could be used with a Type Safe Enumeration in anything below 1.5.
Lets start by defining the database table
SHAPE {
name varchar2(25),
color number(100)
}
As you see the color is stored as a number, but in the class Shape is is a Color object. There are two question that I ran into. First, how do you cleanly map the number stored in the database with the instance of a Color? Second, how can iBatis and custom type handlers help?
To answer the first let me show you the code for the Shape class.
/* * Shape.java * * Created on September 23, 2005 */ package domain; /** * * @author nmaves */ public class Shape { public enum Color { BLUE(1, "0000FF"), RED(2, "FF0000"), GREEN(3, "00FF00"); private int id; private String rgb; private Type(int id, String name) { this(id, name, "no mime type"); } private Type(int id, String rgb) { this.id = id; this. rgb = rgb; } public int getId() { return this.id; } public String getRGB() { return this.name; } public static Type getInstance(int i) { for(Color color : Color.values()) { if(color.id == i) { return type; } } throw new IllegalArgumentException("No such Color"); } } private String name; private Color color; /** Creates a new instance of Frequency */ private Shape(String name, Color color) { this.color = color; this.name = name; } public String toString() { return this.name; } public void setName(String name) { this.name = name; } public void setColor(Color color) { this.color = color; } public String getName() { return this.name; } public Color getColor() { return this.color; } }
As you see the getInstance(int i) method allows for the mapping of the number stored in the database to an instance of this class. I am sure there is a better way of pulling these initial value from a properties file but this is only an example.
For the second part you will need a way to allow iBatis to make the conversion. This is where the custom type handler comes into play.
Below is my ColorTypeHandler.java
package dao.ibatis; import com.ibatis.sqlmap.client.extensions.ParameterSetter; import com.ibatis.sqlmap.client.extensions.ResultGetter; import com.ibatis.sqlmap.client.extensions.TypeHandlerCallback; import java.sql.SQLException; import java.sql.Types; import Shape.Color; public class ColorTypeHandler implements TypeHandlerCallback { public Object getResult(ResultGetter getter) throws SQLException { int value = getter.getInt(); if (getter.wasNull()) { return null; } Color color = Color.getInstance(value); return color; } public void setParameter(ParameterSetter setter, Object parameter) throws SQLException { if (parameter == null) { setter.setNull(Types.INTEGER); } else { Color color = (Color) parameter; setter.setInt(color.getId()); } } public Object valueOf(String s) { return s; } }
You are almost there! All you need left to do is tell iBatis to map instances of Frequency object to this handler.
I added the following line into my SqlMapConfig file.
<typeHandler javaType="domain.Shape$Color" callback="dao.ibatis.ColorTypeHandler"/>
| Useful Information Notice the $ in the class name. This is how Java 1.5 denotes enums. If this were a type safe enum you would just use standard notation. |
That is it. No need to change any of your other sqlmap entries. Here is an example of a select and insert that use this handler.
<resultMap class="Shape" id="ShapeResult"> <result column="name" property="name" /> <result column="color" property="color" /> </resultMap> <select id="getShapeByName" parameterClass="string" resultMap="ShapeResult"> SELECT * FROM SHAPE WHERE name = #value# </select> <insert id="insertReport" parameterClass="Report"> INSERT INTO SHAPE ( name, color ) values ( #name#, #color#, ) </insert>
Notice that there is nothing special that need to be done for the color columns.
Update:
if (getter.wasNull())
threw an exception for me.
Replacing it with:
if (getter.getObject() == null)
works.
The getResult() method above is broken. When calling getter.getNull before actually getting the column's value (getter.getInt(), getNull() will always return true, since no value has been fetched yet. The check for null has to be done after getting the value.
So the correct version of the getResult() from above would look like this:
public Object getResult(ResultGetter getter) throws SQLException { int value = getter.getInt(); if (getter.wasNull()) { return null; } Color color = Color.getInstance(value); return color; }
Top Shape class is wrong, and quite confusing, maybe this way will be cleaner to understand
public class Shape { public enum Color { BLUE(1, "0000FF"), RED(2, "FF0000"), GREEN(3, "00FF00"); private int id; private String rgb; private Color(int id, String rgb) { this.id = id; this. rgb = rgb; } public int getId() { return this.id; } public String getRgb() { return this.rgb; } public static Color getInstance(int i) { for(Color color : Color.values()) { if(color.id == i) { return color; } } throw new IllegalArgumentException("No such Color"); } } private String name; private Color color; private Shape(String name, Color color) { this.color = color; this.name = name; } public String toString() { return this.name; } public void setName(String name) { this.name = name; } public void setColor(Color color) { this.color = color; } public String getName() { return this.name; } public Color getColor() { return this.color; } }
Hey, I just read this and thought that the Usefull Information bar could be confusing to some users. I think that the sentence "Notice the $ in the class name. This is how Java 1.5 denotes enums. If this were a type safe enum you would just use standard notation." is technically incorrect. I'm pretty sure the $ sign is used to denote inner classes. If one were to declare an enum on it's own (in Color.java for example) then the fully qualified name would be domain.Color (assuming you created it in the domain package as there is Shape is just a class.
If you just want to store the string value of the Enum in the database, this is what I came up with.
The abstract base class:
public abstract class EnumTypeHandler<E extends Enum> implements TypeHandlerCallback { private Class<E> enumClass_; public EnumTypeHandler(Class<E> enumClass) { enumClass_ = enumClass; } @SuppressWarnings("unchecked") public void setParameter(ParameterSetter setter, Object parameter) throws SQLException { setter.setString(((E) parameter).name()); } public Object getResult(ResultGetter getter) throws SQLException { return valueOf(getter.getString()); } @SuppressWarnings("unchecked") public Object valueOf(String s) { return Enum.valueOf(enumClass_, s); } }
Then your enum-specific class:
public class ColorTypeHandler extends EnumTypeHandler<Color> { public ColorTypeHandler() { super(Color.class); } }
Then the enum-specific type handler declaration:
<typeHandler javaType="Color" callback="ColorTypeHandler"/>
Seems to work.
I have somewhat of the opposite problem. I have a table with multiple columns that need to get mapped to a complex data structure in Java. For example:
public class Circle{ // Assume standard getter/setters for these... private int raduis; private Color color; private Point center; }
So then my table looks like:
SHAPE{
radius int,
red int,
green int,
blue int,
x int,
y int
}
How do I create a resultMap such that red,green,blue can be used to set the Color, and x,y can be used to set the center? (Also parameterMaps to insert/update the Circle).
This example is actually not that useful. What iBATIS really needs in this case is something like EJB3's Embeddable objects. I have the same problem, where I want to store an Amount object (which is a BigDecimal value and a currency code) into two columns on something I've already mapped. Currently this appears to be impossible with iBATIS's mapping callback interfaces (TypeHandlerCallback) since you only have access to a single DB column.
Are there any plans to add this kind of functionality to iBATIS 3? It would really, really be nice.

Here's a way to cut down on the code.
First, define an interface:
Now, when you declare your enum, implement the interface:
We can now make the TypeHandler shown above more generic:
Finally, for any enum which implements HasValue, we can now make a TypeHandler as follows:
All that remains is to register this with Ibatis in the sqlMapConfig: