您的位置:首页 > 其它

深入了解MyBatis参数

2017-06-09 14:02 316 查看
引用地址:https://my.oschina.net/flags/blog/381199

相信很多人可能都遇到过下面这些异常:

"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;
}
}

在这里有个很关键的params,这个参数类型为Map<Integer, String>,他会根据接口方法按顺序记录下接口参数的定义的名字,如果使用@Param 指定了名字,就会记录这个名字,如果没有记录,那么就会使用它的序号作为名字。

例如有如下接口:

List<User> select(@Param('sex')String sex,Integer age);

那么他对应的params如下:

{
0:'sex',
1:'1'
}

继续看上面的convertArgsToSqlCommandParam方法,这里简要说明3种情况:

入参为null或没有时,参数转换为null
没有使用@Param 注解并且只有一个参数时,返回这一个参数
使用了@Param 注解或有多个参数时,将参数转换为Map1类型,并且还根据参数顺序存储了key为param1,param2的参数。
注意:从第3种情况来看,建议各位有多个入参的时候通过@Param 指定参数名,方便后面(动态sql)的使用。

经过上面方法的处理后,在MapperMethod中会继续往下调用命名空间方式的方法:

Object param = method.convertArgsToSqlCommandParam(args);
result = sqlSession.<E>selectList(command.getName(), param);

从这之后开始按照统一的方式继续处理入参。

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就是我们经过前面两步处理后的参数。这个参数经过前面两步处理后,到这里的时候,他只有下面三种情况:

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

static class ContextMap extends HashMap<String, Object> {
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应该不会有参数名方面的问题了。

<select id="selectOrderby" resultType="User"> select * from user order by ${value} </select>

这种情况下,虽然没有指定一个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,可以这么使用:

<select id="selectUser" resultType="User">
select * from user where enable = ${@com.abel533.mybatis.Code@ENABLE}
</select>


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

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的地方。
内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息
标签: