• UID297
  • 登录2017-10-11
  • 粉丝44
  • 关注112
  • 发帖277
  • 主页http://weibo.com/2619518393/
  • 金币1538枚
喜欢达人
社区居民
最爱沙发
忠实会员
原创写手
追星一族
jkxqj 发布于2015-11-25 15:26
1/1464

深入了解MyBatis参数

楼层直达

深入了解MyBatis参数

相信很多人可能都遇到过下面这些异常:
  • "Parameter 'xxx' not found. Available parameters are [...]"
  • "Could not get property 'xxx' from xxxClass. Cause:
  • "The expression 'xxx' evaluated to a null value."
  • "Error evaluating expression 'xxx'. Return value (xxxxx) was not iterable."
不只是上面提到的这几个,我认为有很多的错误都产生在和参数有关的地方。
想要避免参数引起的错误,我们需要深入了解参数。
想了解参数,我们首先看MyBatis处理参数和使用参数的全部过程。
本篇由于为了便于理解和深入,使用了大量的源码,因此篇幅较长,需要一定的耐心看完,本文一定会对你起到很大的帮助。


参数处理过程

处理接口形式的入参

在使用MyBatis时,有两种使用方法。一种是使用的接口形式,另一种是通过SqlSession调用命名空间。这两种方式在传递参数时是不一样的,命名空间的方式更直接,但是多个参数时需要我们自己创建Map作为入参。相比而言,使用接口形式更简单。
接口形式的参数是由MyBatis自己处理的。如果使用接口调用,入参需要经过额外的步骤处理入参,之后就和命名空间方式一样了。
MapperMethod.java会首先经过下面方法来转换参数:

public Object convertArgsToSqlCommandParam(Object[] args) { final int paramCount = params.size(); if (args == null || paramCount == 0) { return null;
  } else if (!hasNamedParameters && paramCount == 1) { return args[params.keySet().iterator().next()];
  } else { final Map<String, Object> param = new ParamMap<Object>(); int i = 0; for (Map.Entry<Integer, String> entry : params.entrySet()) {
      param.put(entry.getValue(), args[entry.getKey()]); // issue #71, add param names as param1, param2...but ensure backward compatibility 
      final String genericParamName = "param" + String.valueOf(i + 1); if (!param.containsKey(genericParamName)) {
        param.put(genericParamName, args[entry.getKey()]);
      }
      i++;
    } return param;
  }
}




这里特别需要注意的一个地方是map.put("collection", object),这个设计是为了支持Set类型,需要等到MyBatis 3.3.0版本才能使用。
wrapCollection处理的是只有一个参数时,集合和数组的类型转换成Map2类型,并且有默认的Key,从这里你能大概看到为什么<foreach>中默认情况下写的arraylist(Map类型没有默认值map)。


参数的使用

参数的使用分为两部分:
  • 第一种就是常见#{username}或者${username}。
  • 第二种就是在动态SQL中作为条件,例如<if test="username!=null and username !=''">。
下面对这两种进行详细讲解,为了方便理解,先讲解第二种情况。


在动态SQL条件中使用参数

关于动态SQL的基础内容可以查看官方文档
动态SQL为什么会处理参数呢?
主要是因为动态SQL中的<if>,<bind>,<foreache>都会用到表达式,表达式中会用到属性名,属性名对应的属性值如何获取呢?获取方式就在这关键的一步。不知道多少人遇到Could not get property xxx from xxxClass: Parameter ‘xxx’ not found. Available parameters are[…],都是不懂这里引起的。
DynamicContext.java中,从构造方法看起:

public DynamicContext(Configuration configuration, Object parameterObject) { if (parameterObject != null && !(parameterObject instanceof Map)) {
    MetaObject metaObject = configuration.newMetaObject(parameterObject);
    bindings = new ContextMap(metaObject);
  } else {
    bindings = new ContextMap(null);
  }
  bindings.put(PARAMETER_OBJECT_KEY, parameterObject);
  bindings.put(DATABASE_ID_KEY, configuration.getDatabaseId());
}

这里的Object parameterObject就是我们经过前面两步处理后的参数。这个参数经过前面两步处理后,到这里的时候,他只有下面三种情况:
  1. null,如果没有入参或者入参是null,到这里也是null。
  2. Map类型,除了null之外,前面两步主要是封装成Map类型。
  3. 数组、集合和Map以外的Object类型,可以是基本类型或者实体类。
看上面构造方法,如果参数是1,2情况时,执行代码bindings = new ContextMap(null);参数是3情况时执行if中的代码。我们看看ContextMap类,这是一个内部静态类,代码如下:

static class ContextMap extends HashMap&lt;String, Object&gt; { private MetaObject parameterMetaObject; public ContextMap(MetaObject parameterMetaObject) { this.parameterMetaObject = parameterMetaObject;
  } public Object get(Object key) {
    String strKey = (String) key; if (super.containsKey(strKey)) { return super.get(strKey);
    } if (parameterMetaObject != null) { // issue #61 do not modify the context when reading return parameterMetaObject.getValue(strKey);
    } return null;
  }
}


我们先继续看DynamicContext的构造方法,在if/else之后还有两行:

bindings.put(PARAMETER_OBJECT_KEY, parameterObject);
bindings.put(DATABASE_ID_KEY, configuration.getDatabaseId());

其中两个Key分别为:

public static final String PARAMETER_OBJECT_KEY = "_parameter"; public static final String DATABASE_ID_KEY = "_databaseId";


也就是说1,2两种情况的时候,参数值只存在于"_parameter"的键值中。3情况的时候,参数值存在于"_parameter"的键值中,也存在于bindings本身。

当动态SQL取值的时候会通过OGNL从bindings中获取值。MyBatis在OGNL中注册了ContextMap:

static {
  OgnlRuntime.setPropertyAccessor(ContextMap.class, new ContextAccessor());
}

当从ContextMap取值的时候,会执行ContextAccessor中的如下方法:

@Override public Object getProperty(Map context, Object target, Object name) throws OgnlException {
  Map map = (Map) target;

  Object result = map.get(name); if (map.containsKey(name) || result != null) { return result;
  }

  Object parameterObject = map.get(PARAMETER_OBJECT_KEY); if (parameterObject instanceof Map) { return ((Map)parameterObject).get(name);
  } return null;
}

参数中的target就是ContextMap类型的,所以可以直接强转为Map类型。
参数中的name就是我们写在动态SQL中的属性名。

下面举例说明这三种情况:
  • null的时候:
    不管name是什么(name="_databaseId"除外,可能会有值),此时Object result = map.get(name);得到的result=null。
    在Object parameterObject = map.get(PARAMETER_OBJECT_KEY);中parameterObject=null,因此最后返回的结果是null。
    在这种情况下,不管写什么样的属性,值都会是null,并且不管属性是否存在,都不会出错。
  • Map类型:
    此时Object result = map.get(name);一般也不会有值,因为参数值只存在于"_parameter"的键值中。
    然后到Object parameterObject = map.get(PARAMETER_OBJECT_KEY);,此时获取到我们的参数值。
    在从参数值((Map)parameterObject).get(name)根据name来获取属性值。
    在这一步的时候,如果name属性不存在,就会报错:
    throw new BindingException("Parameter '" + key + "' not found. Available parameters are " + keySet());
name属性是什么呢,有什么可选值呢?这就是处理接口形式的入参处理集合处理后所拥有的Key。
如果你遇到过类似异常,相信看到这儿就明白原因了。
数组、集合和Map以外的Object类型:
这种类型经过了下面的处理:
MetaObject metaObject = configuration.newMetaObject(parameterObject);
bindings = new ContextMap(metaObject);



MetaObject是MyBatis的一个反射类,可以很方便的通过getValue方法获取对象的各种属性(支持集合数组和Map,可以多级属性点.访问,如user.username,user.roles[1].rolename)。
现在分析这种情况。
首先通过name获取属性时Object result = map.get(name);,根据上面ContextMap类中的get方法:
public Object get(Object key) {
String strKey = (String) key; if (super.containsKey(strKey)) { return super.get(strKey);
} if (parameterMetaObject != null) { return parameterMetaObject.getValue(strKey);
} return null;
}



可以看到这里会优先从Map中取该属性的值,如果不存在,那么一定会执行到下面这行代码:
return parameterMetaObject.getValue(strKey)

如果name刚好是对象的一个属性值,那么通过MetaObject反射可以获取该属性值。如果该对象不包含name属性的值,就会报错:
throw new ReflectionException("Could not get property '" + prop.getName() + "' from " + object.getClass() + ".  Cause: " + t.toString(), t);

理解这三种情况后,使用动态SQL应该不会有参数名方面的问题了。


在SQL语句中使用参数

SQL中的两种形式#{username}或者${username},虽然看着差不多,但是实际处理过程差别很大,而且很容易出现莫名其妙的错误。
${username}的使用方式为OGNL方式获取值,和上面的动态SQL一样,这里先说这种情况。


${propertyName}参数

TextSqlNode.java中有一个内部的静态类BindingTokenParser,现在只看其中的handleToken方法:

@Override public String handleToken(String content) {
  Object parameter = context.getBindings().get("_parameter"); if (parameter == null) {
    context.getBindings().put("value", null);
  } else if (SimpleTypeRegistry.isSimpleType(parameter.getClass())) {
    context.getBindings().put("value", parameter);
  }
  Object value = OgnlCache.getValue(content, context.getBindings());
  String srtValue = (value == null ? "" : String.valueOf(value)); // issue #274 return "" instead of "null" checkInjection(srtValue); return srtValue;
}

从put("value"这个地方可以看出来,MyBatis会创建一个默认为"value"的值,也就是说,在xml中的SQL中可以直接使用${value},从else if可以看出来,只有是简单类型的时候,才会有值。
关于这点,举个简单例子,如果接口为List<User> selectOrderby(String column),如果xml内容为:

&lt;select id="selectOrderby" resultType="User"&gt; select * from user order by ${value} &lt;/select&gt;



这种情况下,虽然没有指定一个value属性,但是MyBatis会自动把参数column赋值进去。
再往下的代码:

Object value = OgnlCache.getValue(content, context.getBindings());
String srtValue = (value == null ? "" : String.valueOf(value));


这里和动态SQL就一样了,通过OGNL方式来获取值。
看到这里使用OGNL这种方式时,你有没有别的想法?
特殊用法:你是否在SQL查询中使用过某些固定的码值?一旦码值改变的时候需要改动很多地方,但是你又不想把码值作为参数传进来,怎么解决呢?你可能已经明白了。
就是通过OGNL的方式,例如有如下一个码值类:


package com.abel533.mybatis; public interface Code{ public static final String ENABLE = "1"; public static final String DISABLE = "0";
}


如果在xml,可以这么使用:

&lt;select id="selectUser" resultType="User"&gt; select * from user where enable = ${@com.abel533.mybatis.Code@ENABLE} &lt;/select&gt;


除了码值之外,你可以使用OGNL支持的各种方法,如调用静态方法。

#{propertyName}参数

这种方式比较简单,复杂属性的时候使用的MyBatis的MetaObject。
DefaultParameterHandler.java中:
public void setParameters(PreparedStatement ps) throws SQLException {
  ErrorContext.instance().activity("setting parameters").object(mappedStatement.getParameterMap().getId());
  List&lt;ParameterMapping&gt; parameterMappings = boundSql.getParameterMappings(); if (parameterMappings != null) { for (int i = 0; i &lt; parameterMappings.size(); i++) {
      ParameterMapping parameterMapping = parameterMappings.get(i); if (parameterMapping.getMode() != ParameterMode.OUT) {
        Object value;
        String propertyName = parameterMapping.getProperty(); if (boundSql.hasAdditionalParameter(propertyName)) { // issue #448 ask first for additional params value = boundSql.getAdditionalParameter(propertyName);
        } else if (parameterObject == null) {
          value = null;
        } else if (typeHandlerRegistry.hasTypeHandler(parameterObject.getClass())) {
          value = parameterObject;
        } else {
          MetaObject metaObject = configuration.newMetaObject(parameterObject);
          value = metaObject.getValue(propertyName);
        }
        TypeHandler typeHandler = parameterMapping.getTypeHandler();
        JdbcType jdbcType = parameterMapping.getJdbcType(); if (value == null && jdbcType == null) {
          jdbcType = configuration.getJdbcTypeForNull();
        }
        typeHandler.setParameter(ps, i + 1, value, jdbcType);
      }
    }
  }
}


上面这段代码就是从参数中取#{propertyName}值的方法,这段代码的主要逻辑就是if/else判断的地方,单独拿出来分析:
if (boundSql.hasAdditionalParameter(propertyName)) { // issue #448 ask first for additional params value = boundSql.getAdditionalParameter(propertyName);
} else if (parameterObject == null) {
  value = null;
} else if (typeHandlerRegistry.hasTypeHandler(parameterObject.getClass())) {
  value = parameterObject;
} else {
  MetaObject metaObject = configuration.newMetaObject(parameterObject);
  value = metaObject.getValue(propertyName);
}

  • 首先看第一个if,当使用<foreach>的时候,MyBatis会自动生成额外的动态参数,如果propertyName是动态参数,就会从动态参数中取值。
  • 第二个if,如果参数是null,不管属性名是什么,都会返回null。
  • 第三个if,如果参数是一个简单类型,或者是一个注册了typeHandler的对象类型,就会直接使用该参数作为返回值,和属性名无关。
  • 最后一个else,这种情况下是复杂对象或者Map类型,通过反射方便的取值。
下面我们说明上面四种情况下的参数名注意事项。
    动态参数,这里的参数名和值都由MyBatis动态生成的,因此我们没法直接接触,也不需要管这儿的命名。但是我们可以了解一下这儿的命名规则,当以后错误信息看到的时候,我们可以确定出错的地方。
    ForEachSqlNode.java中:
    private static String itemizeItem(String item, int i) { return new StringBuilder(ITEM_PREFIX).append(item).append("_").append(i).toString();
    }
其中ITEM_PRFIX为public static final String ITEM_PREFIX = "__frch_";。
如果在<foreach>中的collection="userList" item="user",那么对userList循环产生的动态参数名就是:
__frch_user_0,__frch_user_1,__frch_user_2…

如果访问动态参数的属性,如user.username会被处理成__frch_user_0.username,这种参数值的处理过程在更早之前解析SQL的时候就已经获取了对应的参数值。具体内容看下面有关<foreach>的详细内容。[
参数为null,由于这里的判断和参数名无关,因此入参null的时候,在xml中写的#{name}不管name写什么,都不会出错,值都是null。


可以直接使用typeHandler处理的类型。最常见的就是基本类型,例如有这样一个接口方法User selectById(@Param("id")Integer id),在xml中使用id的时候,我们可以随便使用属性名,不管用什么样的属性名,值都是id。

复杂对象或者Map类型一般都是我们需要注意的地方,这种情况下,就必须保证入参包含这些属性,如果没有就会报错。这一点和可以参考上面有关MetaObject的地方。

<foreach>详解

所有动态SQL类型中,<foreach>似乎是遇到问题最多的一个。
例如有下面的方法:

&lt;insert id="insertUserList"&gt; INSERT INTO user(username,password)
  VALUES &lt;foreach collection="userList" item="user" separator=","&gt; (#{user.username},#{user.password}) &lt;/foreach&gt; &lt;/insert&gt;


对应的接口:

int insertUserList(@Param("userList")List&lt;User&gt; list);



我们通过foreach源码,看看MyBatis如何处理上面这个例子。
ForEachSqlNode.java中的apply方法中的前两行:

Map&lt;String, Object&gt; bindings = context.getBindings(); final Iterable&lt;?&gt; iterable = evaluator.evaluateIterable(collectionExpression, bindings);



这里的bindings参数熟悉吗?上面提到过很多。经过一系列的参数处理后,这儿的bindings如下:

{ "_parameter":{ "param1":list, "userList":list
  }, "_databaseId":null,
}



collectionExpression就是collection="userList"的值userList。
我们看看evaluator.evaluateIterable如何处理这个参数,在ExpressionEvaluator.java中的evaluateIterable方法:
public Iterable&lt;?&gt; evaluateIterable(String expression, Object parameterObject) {
    Object value = OgnlCache.getValue(expression, parameterObject); if (value == null) { throw new BuilderException("The expression '" + expression + "' evaluated to a null value.");
    } if (value instanceof Iterable) { return (Iterable&lt;?&gt;) value;
    } if (value.getClass().isArray()) { int size = Array.getLength(value);
        List&lt;Object&gt; answer = new ArrayList&lt;Object&gt;(); for (int i = 0; i &lt; size; i++) {
            Object o = Array.get(value, i);
            answer.add(o);
        } return answer;
    } if (value instanceof Map) { return ((Map) value).entrySet();
    } throw new BuilderException("Error evaluating expression '" + expression + "'.  Return value (" + value + ") was not iterable.");
}


首先通过看第一行代码:

Object value = OgnlCache.getValue(expression, parameterObject);

这里通过OGNL获取到了userList的值。获取userList值的时候可能出现异常,具体可以参考上面动态SQL部分的内容。
userList的值分四种情况。
  1. value == null,这种情况直接抛出异常BuilderException。
  2. value instanceof Iterable,实现Iterable接口的直接返回,如Collection的所有子类,通常是List。
  3. value.getClass().isArray()数组的情况,这种情况会转换为List返回。
  4. value instanceof Map如果是Map,通过((Map) value).entrySet()返回一个Set类型的参数。
通过上面处理后,返回的值,是一个Iterable类型的值,这个值可以使用for (Object o : iterable)这种形式循环。
在ForEachSqlNode中对iterable循环的时候,有一段需要关注的代码:

if (o instanceof Map.Entry) { @SuppressWarnings("unchecked") 
    Map.Entry&lt;Object, Object&gt; mapEntry = (Map.Entry&lt;Object, Object&gt;) o;
    applyIndex(context, mapEntry.getKey(), uniqueNumber);
    applyItem(context, mapEntry.getValue(), uniqueNumber);
} else {
    applyIndex(context, i, uniqueNumber);
    applyItem(context, o, uniqueNumber);
}



如果是通过((Map) value).entrySet()返回的Set,那么循环取得的子元素都是Map.Entry类型,这个时候会将mapEntry.getKey()存储到index中,mapEntry.getValue()存储到item中。
如果是List,那么会将序号i存到index中,mapEntry.getValue()存储到item中。

<foreach>常见错误补充

当collection="userList"的值userList中的User是一个继承自Map的类型时,你需要保证<foreach>循环中用到的所有对象的属性必须存在,Map类型存在的问题通常是,如果某个值是null,一般是不存在相应的key,这种情况会导致<foreach>出错,会报找不到__frch_user_x参数。所以这种情况下,就是值是null,你也需要map.put(key,null)。

最后

这篇文章很长,写这篇文章耗费的时间也很长,超过10小时,写到半夜两点都没写完。
这篇文章真的非常有用,如果你对Mybatis有一定的了解,这篇文章几乎是必读的一篇。
如果各位发现文中错误或者其他问题欢迎留言或加群详谈。


MyBatis分页插件
http://git.oschina.net/free/Mybatis_PageHelper
MyBatis通用Mapper
http://git.oschina.net/free/Mapper
Mybatis专栏:
作者博客:
作者QQ: 120807756
作者邮箱: abel533@gmail.com
Mybatis工具群: 211286137 (欢迎各位加群)

原文来自:http://blog.csdn.net/isea533/article/details/44002219
欢迎关注我的微信公众号:oldriverNote

0人打赏
  • UID297
  • 登录2017-10-11
  • 粉丝44
  • 关注112
  • 发帖277
  • 主页http://weibo.com/2619518393/
  • 金币1538枚
喜欢达人
社区居民
最爱沙发
忠实会员
原创写手
追星一族
jkxqj 发布于2015-11-29 12:08
沙发F
沙发!
欢迎关注我的微信公众号:oldriverNote
您需要登录后才可以回帖
发表回复
极贡献
技术问答
专题荟萃
程序人生
视觉设计
Android开发
iOS开发
编程语言
前端开发
后端开发
服务器架构
软件测试
运维方案
创业路上



最热文章墙

  • 77037/375   【精品推荐】200多种Android动画效果的强悍框架,太全了,不看这个,再有动画的问题,不理你了^@^

  • 44087/191   情人节福利,程序员表白的正确姿势:改几行代码就变成自己的表白了

  • 43710/0   Python爬虫:常用浏览器的useragent

  • 40229/259   【精品推荐】Android版产品级的音乐播放器源码,功能太强大了,最好的产品原型有木有?

  • 37959/145   省时省力的Android组件群来了,非常棒的原型参考

  • 29226/142   2016抢红包软件及源码

  • 28978/2   超全!整理常用的iOS第三方资源

  • 28850/71   原创表白APP,以程序员的姿势备战新年后的7夕,持续完善中!

  • 23573/159   Android版类似UC浏览器:非常赞,产品级的源码

  • 22589/30   麻省理工的一帮疯子,真的实现了随意操控万物!(绝对黑科技)

  • 22273/25   Android工程师面试题大全

  • 22148/27   2016程序员跳槽全攻略

  • 21740/9   GitHub上排名前50的iOS项目:总有一款你用得着

  • 20703/20   码魂:程序员的牛B漫画

  • 18795/10   2016年最全的Android面试考题+答案 精编版

  • 18647/85   Android小而全的博客源码:非常适合全面掌握开发技巧

  • 18530/3   吐槽那些程序员的搞笑牛逼注释

  • 18524/42   一个绚丽的loading动效分析与实现!

  • 18504/73   【持续更新中】Android福利贴(二):资料源码大放送

  • 17454/1   iOS 动画总结

  • 17302/45   惊艳的App引导页:背景图片切换加各个页面动画效果

  • 17103/81   仿京东商城客户端Android最新版,不错的原型和学习资料

  • 16930/104   Android带弹幕的视频播放器源码,来自大名鼎鼎的Bilibili弹幕网站

  • 16842/23   个人收集的Android 各类功能源代码

  • 16330/5   新一代Android渠道打包工具:1000个渠道包只需要5秒

  • 16249/10   女程序员的梦,众网友的神回复

  • 16242/21   Android福利第三波【Android电子书】

  • 16111/53   基于瀑布流的美女图片浏览App,有注释的源代码

  • 16086/17   用JavaScript 来开发iOS和Android 原生应用:React Native开源框架中文版来啦

  • 16005/81   【精品推荐】类似360安全卫士安Android源码:非常赞的产品原型

  • 15939/11   年会上现场review代码是怎么样的体验!

  • 15750/23   珍藏多年的素材,灵感搜寻网站

  • 15748/0   iOS中文版资源库,非常全

  • 15048/18   65条最常用正则表达式,你要的都在这里了

  • 14575/15   基于Android支付宝支付设计和开发方案

  • 13999/17   什么是真正的黑客:收获12200+Stars,人气远超微软开源VS

  • 13972/11   有木有这样一张酷图帮你集齐所有git命令超实用

  • 13658/46   在线音乐播放器完整版(商用级的源码):非常赞,可听免费高品质专辑

  • 13483/0   GitHub iOS 库和框架Top100 

  • 13333/7   一张图搞定iOS学习路线,非常全面

  • 13326/7   用程序员的姿势抢过年的火车票

  • 13202/61   【技巧一】搭配Android Studio,如何实现App远程真机debug?

  • 12958/10   成为Java顶尖程序员 ,看这11本书就够了

  • 12877/10   微信支付终于成功了(安卓,iOS),在此分享

  • 12801/18   一张图搞定Android学习路线,非常全面

  • 12542/29   【持续更新中】Android福利贴(一):资料源码

  • 12477/3   基于Node.js的强大爬虫,能直接发布抓取的文章哦

  • 12188/4   46 个非常有用的 PHP 代码片段

  • 11744/3   即时通信第三方库

  • 11257/8   流媒体视频直播方案

  • 11172/18   八个最优秀的Android Studio插件

  • 11054/9   B站建开源工作组:APP想支持炫酷弹幕的看过来

  • 10884/9   烧了5亿美金,这家神秘的公司即将颠覆人类未来!

  • 10785/2   【精品推荐】高质量PHP代码的50个实用技巧:非常值得收藏

  • 10696/10   中国黑客的隐秘江湖:攻守对立,顶尖高手月入千万美元

  • 10059/6   开箱即用!Android四款系统架构工具

  • 9878/10   十大技巧快速提升Android应用开发性能

  • 9799/3   10款GitHub上最火爆的国产开源项目——可以媲美西半球

  • 9698/1   Android性能优化视频,文档以及工具

  • 9660/3   一张图看清Linux 内核运行原理

  • 返回顶部