Ibatis 关于处理model中list属性,要将list中的值转化为string存储到数据库中
Model中的属性是
private List<TimeSliceTO> timeSlice;
数据库存储为:varchar(1000) TIMESLICE
在自定义的typeHandler中定义方法
package batis.typehandler;
public class TimeSliceTOHandler implements TypeHandlerCallback {
@Override
public void setParameter(ParameterSetter setter, Object parameter) throws SQLException {
//这里实现list转成string存储到数据库的值
}
sqlmap的添加或者编辑方法
#timeSlice,handler=ibatis.typehandler.SaleTimeSliceTOHandler#,
resultMap
<result column="TIMESLICE" jdbcType="VARCHAR" property="timeSlice" javaType="java.util.List" typeHandler="ibatis.typehandler.SaleTimeSliceTOHandler"/>
这个是处理list作为属性的,然后将list处理之后存储到数据库中的过程
关键是添加和编辑时用到的handler是和setParameter方法同时使用的。
resultMap中的typeHandler是查询得到结果用的,配合TimeSliceTOHandler 处理器
public Object getResult(ResultGetter getter) throws SQLException {}
这个方法使用的,
他们是相互对应关系的。如果用 java.util.List.作为全局的typeHandler来处理,会带来一些不必要的麻烦,各有各的好处吧。
下面是一些ibatis读取源码相关的逻辑,如果上面的看不懂,可以学习下下面的然后就会明白了。
下午写了一个DO类,里面有一个枚举的对象,因此用到了ibatis的自定义handler
Account类里的status属性是枚举类型,对应的xml文件如下:
- ............
- <typeAlias alias="account" type="test.Account"/>
- <insert id="insertAccount" parameterClass="account">
- insert into ACCOUNT (
- ACC_ID,
- ACC_FIRST_NAME,
- ACC_LAST_NAME,
- STATUS)
- values (
- #id#,
- #firstName#,
- #lastName#,
- #status,handler=test.MyHandler#
- )
- </insert>
- .................
上面的status是枚举类,所以在总的配置文件sqlmapconfig.xml中配置了handler
- ......
- <typeHandler javaType="test.Account" jdbcType="VARCHAR" callback="test.MyHandler"/>
- ........
以上这么配置,是没有问题的,项目里面的handler也都是这么配置,以前没有仔细深究,今天仔细看了看。有个疑问:
为什么要配置2处?有这个必要么?
于是乎,回来翻了一下ibatis的源代码,释然了。
首先看看ibatis解析xml文件的代码:
com.ibatis.sqlmap.engine.mapping.parameter.InlineParameterMapParser
- public SqlText parseInlineParameterMap(TypeHandlerFactory typeHandlerFactory, String sqlStatement, Class parameterClass) {
- .....
- StringTokenizer parser = new StringTokenizer(sqlStatement, PARAMETER_TOKEN, true);
- StringBuffer newSqlBuffer = new StringBuffer();
- //这个while循环把#aaa#,#bbb#,#ccc#这种东西替换成 ?,?,?,并解析##之间的内容
- while (parser.hasMoreTokens()) {
- token = parser.nextToken();
- if (PARAMETER_TOKEN.equals(lastToken)) {
- if (PARAMETER_TOKEN.equals(token)) {
- newSqlBuffer.append(PARAMETER_TOKEN);
- token = null;
- } else {
- ParameterMapping mapping = null;
- //PARAM_DELIM=":"
- if (token.indexOf(PARAM_DELIM) > -1) {
- mapping = oldParseMapping(token, parameterClass, typeHandlerFactory);
- } else {
- //这里面解析##之间的内容,返回一个mapping
- mapping = newParseMapping(token, parameterClass, typeHandlerFactory);
- }
- mappingList.add(mapping);
- newSqlBuffer.append("?");
- boolean hasMoreTokens = parser.hasMoreTokens();
- if (hasMoreTokens)
- token = parser.nextToken();
- if (!hasMoreTokens || !PARAMETER_TOKEN.equals(token)) {
- throw new SqlMapException(
- "Unterminated inline parameter in mapped statement near '"
- + newSqlBuffer.toString() + "'");
- }
- token = null;
- }
- } else {
- if (!PARAMETER_TOKEN.equals(token)) {
- newSqlBuffer.append(token);
- }
- }
- lastToken = token;
- }
- ....
- }
迫不及待的进入到newParseMapping方法中
一大堆的if..else,慢慢看来
- private ParameterMapping newParseMapping(String token, Class parameterClass, TypeHandlerFactory typeHandlerFactory) {
- ParameterMapping mapping = new ParameterMapping();
- /*同样是用StringTokenizer,不过这里的分割符号为"="或者","
- *所以如果##之间的字符串是
- *propertyName,javaType=string,jdbcType=VARCHAR,mode=IN,nullValue=N/A,handler=string,numericScale=2
- 那么解析出来就是propertyName javaType string jdbcType ......
- */
- StringTokenizer paramParser = new StringTokenizer(token, "=,", false);
- //第一个显然是参数的名字
- mapping.setPropertyName(paramParser.nextToken());
- while (paramParser.hasMoreTokens()) {
- String field = paramParser.nextToken();
- if (paramParser.hasMoreTokens()) {
- String value = paramParser.nextToken();
- if ("javaType".equals(field)) {
- value = typeHandlerFactory.resolveAlias(value);
- mapping.setJavaTypeName(value);
- } else if ("jdbcType".equals(field)) {
- mapping.setJdbcTypeName(value);
- } else if ("mode".equals(field)) {
- mapping.setMode(value);
- } else if ("nullValue".equals(field)) {
- mapping.setNullValue(value);
- } else if ("handler".equals(field)) {
- try {
- //到了handler这一快,在例子中,我们指定了handler
- //所以,这里直接通过反射,给我们造了一个出来。
- //由此可见,如果##之间如果有配handler,则会优先用这个,外面定义的handler在这里不起任何作用
- //如果我们没有在这里配置handler呢?接着往下看
- value = typeHandlerFactory.resolveAlias(value);
- Object impl = Resources.instantiate(value);
- if (impl instanceof TypeHandlerCallback) {
- mapping.setTypeHandler(new CustomTypeHandler((TypeHandlerCallback) impl));
- } else if (impl instanceof TypeHandler) {
- mapping.setTypeHandler((TypeHandler) impl);
- } else {
- throw new SqlMapException ("The class " + value + " is not a valid implementation of TypeHandler or TypeHandlerCallback");
- }
- } catch (Exception e) {
- throw new SqlMapException("Error loading class specified by handler field in " + token + ". Cause: " + e, e);
- }
- } else if ("numericScale".equals(field)) {
- try {
- Integer numericScale = Integer.valueOf(value);
- if (numericScale.intValue() < 0) {
- throw new SqlMapException("Value specified for numericScale must be greater than or equal to zero");
- }
- mapping.setNumericScale(numericScale);
- } catch (NumberFormatException e) {
- throw new SqlMapException("Value specified for numericScale is not a valid Integer");
- }
- } else {
- throw new SqlMapException("Unrecognized parameter mapping field: '" + field + "' in " + token);
- }
- } else {
- throw new SqlMapException("Incorrect inline parameter map format (missmatched name=value pairs): " + token);
- }
- }
- //如果没有配置handler,这里会根据parameterClass给一个UnkownTypeHandler对象
- //一般我们都会定义参数的parameterClass,所以关键看看那个else里面发生了什么。
- if (mapping.getTypeHandler() == null) {
- TypeHandler handler;
- if (parameterClass == null) {
- handler = typeHandlerFactory.getUnkownTypeHandler();
- } else {
- //又调用resolveTypeHandler
- handler = resolveTypeHandler(typeHandlerFactory, parameterClass, mapping.getPropertyName(), mapping.getJavaTypeName(), mapping.getJdbcTypeName());
- }
- mapping.setTypeHandler(handler);
- }
- return mapping;
- }
再看看这个方法:resolveTypeHandler
- private TypeHandler resolveTypeHandler(TypeHandlerFactory typeHandlerFactory, Class clazz, String propertyName, String javaType, String jdbcType) {
- TypeHandler handler = null;
- if (clazz == null) {
- // Unknown
- handler = typeHandlerFactory.getUnkownTypeHandler();
- } else if (DomTypeMarker.class.isAssignableFrom(clazz)) {
- // DOM
- handler = typeHandlerFactory.getTypeHandler(String.class, jdbcType);
- } else if (java.util.Map.class.isAssignableFrom(clazz)) {
- // Map
- if (javaType == null) {
- handler = typeHandlerFactory.getUnkownTypeHandler(); //BUG 1012591 - typeHandlerFactory.getTypeHandler(java.lang.Object.class, jdbcType);
- } else {
- try {
- javaType = typeHandlerFactory.resolveAlias(javaType);
- Class javaClass = Resources.classForName(javaType);
- handler = typeHandlerFactory.getTypeHandler(javaClass, jdbcType);
- } catch (Exception e) {
- throw new SqlMapException("Error. Could not set TypeHandler. Cause: " + e, e);
- }
- }
- } else if (typeHandlerFactory.getTypeHandler(clazz, jdbcType) != null) {
- // Primitive
- handler = typeHandlerFactory.getTypeHandler(clazz, jdbcType);
- } else {
- //关键在这里,根据配置,我们在##之间显然没有配置javatype,显然为空
- if (javaType == null) {
- //class是parameterclass,这里通过该class的get方法获取这个枚举类的class类型
- Class type = PROBE.getPropertyTypeForGetter(clazz, propertyName);
- //接着,根据这个class,和jdbcType去factory里面查,此时jdbcType是空的
- handler = typeHandlerFactory.getTypeHandler(type, jdbcType);
- } else {
- try {
- //如果配置了javaType,则根据class类型从factory直接获取之,factory后面会讲到
- javaType = typeHandlerFactory.resolveAlias(javaType);
- Class javaClass = Resources.classForName(javaType);
- handler = typeHandlerFactory.getTypeHandler(javaClass, jdbcType);
- } catch (Exception e) {
- throw new SqlMapException("Error. Could not set TypeHandler. Cause: " + e, e);
- }
- }
- }
- return handler;
- }
再来看看factory的代码,很简单:
- public TypeHandler getTypeHandler(Class type, String jdbcType) {
- /*这个typeHandlerMap里面包含了所有的typeHandler,如:
- *基本对象的handler,还有我们自定义在statement外的handler
- *factory其实就是一个Map<class,Map<jdbcType,Handler>>
- *一个对象可以对应多种jdbcType的handler
- */
- Map jdbcHandlerMap = (Map) typeHandlerMap.get(type);
- TypeHandler handler = null;
- //回到例子中来,我们通过class=test.MyHandler,显然能够找到一个Map<jdbcType,Handler>
- if (jdbcHandlerMap != null) {
- //接着,就杯具了。。。。
- /*直接用jdbcType这个空的对象去map里面取,如果我们的type是ibatis内置的对象或者基本类型还好,它会再初始化的时候插入一个key=null,value=基本的handler(如:IntegerHandler,LongHandler) 的记录进去,因此,对于一些string,interger等属性,即使不写jdbctype,也是可以正常的获得handler的。而此时,我们的jdbcType是空的,由于枚举类是自定义的,故:对应的map里面没有放置key为null的默认handler,而是放置了一个key=VARCHAR,value为test.TypeHandler的东东。所以,取出来的handler为空
- */
- handler = (TypeHandler) jdbcHandlerMap.get(jdbcType);
- if (handler == null) {
- //再取一次,也是徒劳
- handler = (TypeHandler) jdbcHandlerMap.get(null);
- }
- }
- //新版的ibatis增加了默认的枚举handler,如果是这样就没有问题了
- //可惜公司用的ibatis是木有下面这几行代码的。
- if (handler == null && type != null && Enum.class.isAssignableFrom(type)) {
- handler = new EnumTypeHandler(type);
- }
- return handler;
- }
提一下UnknownTypeHandler
上面提到的是非动态的sql,如果是动态的sql,则会在解析完xml文件后,所有的变量的handler都会设置成UnknownTypeHandler
它的代setParameter码如下:
- public void setParameter(PreparedStatement ps, int i, Object parameter, String jdbcType)
- throws SQLException {
- /*
- parameter是传递进来的变量,有可能是map里面的也可能是参数类里面的
- jdbcType 是##之间的设置的属性,如:#status,jdbcType=VARCHAR#
- */
- //直接读取参数的类型
- Class searchClass = parameter.getClass();
- 这个默认是false
- if ( usingJavaPre5 ) {
- try {
- searchClass = getBaseClass(searchClass);
- }
- catch ( Exception ex ) {
- searchClass = null;
- }
- }
- if ( searchClass == null ) {
- searchClass = parameter.getClass();
- }
- //仍然是通过参数的class和jdbcType来获取handler
- TypeHandler handler = factory.getTypeHandler(searchClass, jdbcType);
- handler.setParameter(ps, i, parameter, jdbcType);
- }
总结:
- handler是通过javatype和jdbctype来寻找的,如果javatype为空,则自动通过parameterclass对象的get方法来获取。而jdbctype只能通过##之间的配置获取,如果这个为空,并且又没有在##之间指定handler,肯定找不到handler
- 在##之间指定的handler会优先使用,木有则回去factory里根据javatype和jdbctype获取。
- 在新版(相对于我的项目中的ibatis版本)的ibatis中,枚举类有默认的handler实现,可以不用写handler(这个没自己试过,只是源码里面有)
- 查询的statement根据resultmap里面的定义来调用handler
回到例子中:
如果在statement之外木有定义handler,则需要在##之间这些写
- #status,handler=test.MyHandler#
如果定义了handler,则只需要增加一个jdbctype的属性,即可找到handler
显然,这种写法比上面的好,可以复用。
- #status,jdbcType=VARCHAR#
注意:
对于接口类型的javatype,在配置文件里面的javatype最好写成实现类的class:
如有这么一个类
- public class Account{
- ......
- //这个字段使用自定义的handler来提交(设置返回)数据
- private List<String> names;
- ......
- //set方法
- public void setNames(List<String> names){
- this.names=names;
- }
- //get方法
- public List<String> getNames(){
- return names;
- }
- }
总的sqlmapconfig文件中,这么配置
- .................
- <!--这里也是用java.util.arrayList-->
- <typeHandler jdbcType="VARCHAR" javaType="java.util.ArrayList" callback="aaa.bbb.cc.eee.ffff" />
- .................
在对应的ibatis sql映射文件中,其javaType必须写成
- <resultMap id="aaaa" class="aaa">
- ...........
- <!-- 这里写成java.util.ArrayList 否则查询数据的时候会报错
- 找不到handle,导致空指针。因为对应的handlerFactory里面
- class存的是ArrayList
- -->
- <result property="incomeTypes" column="aaa" jdbcType="VARCHAR" javaType="java.util.ArrayList" />
- ..........
- <!-- 下面的一些statement 也配置成java.util.arrayList-->
- <insert>
- .......
- #names,jdbcType=VARCHAR,javajavaType=java.util.ArrayList#,
- ....
- </insert>
ibatis中有一个TypeHandler(准确的说应该是TypeHandlerCallback), 这个接口一般用的比较少, google一下, 大部分就是用来将数据库中的值与java的枚举值或者clob与字符串之间的转换. 最近本人也用到了这个东东. 不过我们使用的是将保存在数据库中以一定分隔符连接的字符串转换成List类型. 开始不知道有TypeHandler这个东东, 于是在JavaBean中定义了两个属性, 一个string类型的, 一个List类型的, 然后内部之间转换, 这种做法有一个弊端, 就是实际上在对一个属性操作的时候, 会有两个接口, 一方面给使用方造成了困惑, 另一个维护起来也不方便, 于是将转换过程完全封装, 对外提供提一个访问接口, 成了一个必要的选择.
先来看看TypeHandlerCallback的定义:
- public interface TypeHandlerCallback {
- public void setParameter(ParameterSetter setter, Object parameter)
- throws SQLException;
- public Object getResult(ResultGetter getter)
- throws SQLException;
- public Object valueOf(String s);
代码很好理解, 而且为了说明如何使用, 作者不惜在注视中给出了一个example.
代码很好理解, setParameter()方法主要是给PrepareStatement赋值. 因此是在insert, update, delete这些操作的时候, 指定传递参数用的.
getResult()方法用来将ResultSet结果集中的内容转换到JavaBean中对应的属性.
valueOf()主要是用来当没有指定的值的时候, 指定默认值的. 主要是ResultSet到JavaBean之间的转换的时候会用到. 最好不要返回null值, 它跟nullValue相关.
下面是一个将数据库中";"分隔的字符串与DO中的list对象之间的转换的实现:
- public class PropertiesTypeHandlerCallback implements TypeHandlerCallback {
- private static final String LIST_SPLIT_FLAG = ";";
- public Object getResult(ResultGetter getter) throws SQLException {
- String properties = getter.getString();
- return CollectionUtils.stringToList(properties, LIST_SPLIT_FLAG, new
- StringConvertor<Property>() {
- public Property convert(String str) {
- return Property.valueOf(str);
- }
- });
- }
- @SuppressWarnings("unchecked")
- public void setParameter(ParameterSetter setter, Object parameter) throws SQLException {
- List<String> propertyList = (List<String>) parameter;
- setter.setString(CollectionUtils.listToString(propertyList, LIST_SPLIT_FLAG));
- }
- public Object valueOf(String s) {
- return Collections.EMPTY_LIST;
- }
- }
接下来是sqlmap的映射文件中进行配置.
针对sql返回结果的转换, 需要在对应的resultMap中指定一下, 比如这样写:
- <result property="propertyList" column="PROPERTIES" typeHandler="com.mysoft.dao.ibatis.support.PropertiesTypeHandlerCallback"/>
针对赋值的参数的设置, 有两种做法, 一种是在parameterMap中的parameter标签中的设置typeHandler属性为对应的callback, 这种做法有一种不好的地方, 就是不同的赋值map, 需要单独定义, 这个反而增加了工作量. 而且使用parameterMap之后, sql语句中的原来在##中指定赋值名称需要改成?, 赋值遵循parameterMap中的定义顺序, 这种做法既繁琐, 可维护性也不好, 因此推荐采用inlineParameter的做法, 比如这样写:
- #propertyList,handler=com.taobao.item.dao.ibatis.support.ItemVerticalPropertiesTypeHandlerCallback#
对于TypeHandler的配置, 还有一种做法, 就是在sqlmap中对某一种类型, 使用typeHandler标签进行定义, 比如这样写:
- <typeHandler javaType="" callback=""/>