深入学习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;
 }
}

在这里有个很关键的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);

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

处理集合

不管是selectOne还是selectMap方法,归根结底都是通过selectList进行查询的,不管是delete还是insert方法,都是通过update方法操作的。在selectList和update中所有参数的都进行了统一的处理。

在DefaultSqlSession.java中的wrapCollection方法:

private Object wrapCollection(final Object object) {
 if (object instanceof Collection) {
 StrictMap<Object> map = new StrictMap<Object>();
 map.put("collection", object);
 if (object instanceof List) {
 map.put("list", object);
 }
 return map;
 } else if (object != null && object.getClass().isArray()) {
 StrictMap<Object> map = new StrictMap<Object>();
 map.put("array", object);
 return map;
 }
 return object;
}

这里特别需要注意的一个地方是map.put("collection", object) ,这个设计是为了支持Set类型,需要等到MyBatis 3.3.0版本才能使用。

wrapCollection处理的是只有一个参数时,集合和数组的类型转换成Map2类型,并且有默认的Key,从这里你能大概看到为什么<foreach>中默认情况下写的array和list(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就是我们经过前面两步处理后的参数。这个参数经过前面两步处理后,到这里的时候,他只有下面三种情况:

  • 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中的属性名。

下面举例说明这三种情况:

1、null的时候:

不管name是什么(name="_databaseId"除外,可能会有值),此时Object result = map.get(name);得到的result=null

Object parameterObject = map.get(PARAMETER_OBJECT_KEY);parameterObject=null,因此最后返回的结果是null。

在这种情况下,不管写什么样的属性,值都会是null,并且不管属性是否存在,都不会出错。

2、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。

如果你遇到过类似异常,相信看到这儿就明白原因了。

3、数组、集合和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内容为:

<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支持的各种方法,如调用静态方法。

#{propertyName}参数

这种方式比较简单,复杂属性的时候使用的MyBatis的MetaObject。

在DefaultParameterHandler.java中:

public void setParameters(PreparedStatement ps) throws SQLException {
 ErrorContext.instance().activity("setting parameters").object(mappedStatement.getParameterMap().getId());
 List<ParameterMapping> parameterMappings = boundSql.getParameterMappings();
 if (parameterMappings != null) {
 for (int i = 0; i < 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类型,通过反射方便的取值。

下面我们说明上面四种情况下的参数名注意事项。

1、动态参数,这里的参数名和值都由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>的详细内容。

2、参数为null,由于这里的判断和参数名无关,因此入参null的时候,在xml中写的#{name}不管name写什么,都不会出错,值都是null。

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

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

<foreach>详解

所有动态SQL类型中, <foreach>似乎是遇到问题最多的一个。

例如有下面的方法:

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

对应的接口:

int insertUserList(@Param("userList")List<User> list);

我们通过foreach源码,看看MyBatis如何处理上面这个例子。

在ForEachSqlNode.java中的apply方法中的前两行:

Map<String, Object> bindings = context.getBindings();
final Iterable<?> 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<?> 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<?>) value;
 }
 if (value.getClass().isArray()) {
  int size = Array.getLength(value);
  List<Object> answer = new ArrayList<Object>();
  for (int i = 0; i < 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的值分四种情况。

  • value == null,这种情况直接抛出异常BuilderException。
  • value instanceof Iterable,实现Iterable接口的直接返回,如Collection的所有子类,通常是List。
  • value.getClass().isArray()数组的情况,这种情况会转换为List返回。
  • value instanceof Map如果是Map,通过((Map) value).entrySet()返回一个Set类型的参数。

通过上面处理后,返回的值,是一个Iterable类型的值,这个值可以使用for (Object o : iterable)这种形式循环。

在ForEachSqlNode中对iterable循环的时候,有一段需要关注的代码:

if (o instanceof Map.Entry) {
 @SuppressWarnings("unchecked")
 Map.Entry<Object, Object> mapEntry = (Map.Entry<Object, Object>) 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)

总结

以上就是这篇文章的全部内容了,这篇文章真的非常有用,如果你对Mybatis有一定的了解,这篇文章几乎是必读的一篇。希望本文的内容对大家的学习或者工作能带来一定的帮助,如有疑问大家可以留言交流,谢谢大家对我们的支持。

(0)

相关推荐

  • Mybatis传list参数调用oracle存储过程的解决方法

    怎么利用MyBatis传List类型参数到数据库存储过程中实现批量插入数据? MyBatis中参数是List类型时怎么处理?大家都知道MyBatis批处理大量数据是很难做到事务回滚的(事务由Spring管理),都将逻辑写在存储中又是及其头疼的一件事(参数长度也有限制),那么我想的是将参数在后台封装为单个或多个list集合,直接通过MyBatis将此参数传到数据库存储过程中,一来摆脱了MyBatis批量插入数据的诸多限制(例如:不能实时返回主键.foreach标签循环集合长度有限制),二来就是在存

  • Java的MyBatis框架中关键的XML字段映射的配置参数详解

    properties 这些是外部化的,可替代的属性,这些属性也可以配置在典型的Java属性配置文件中,或者通过properties元素的子元素来传递.例如: <properties resource="org/mybatis/example/config.properties"> <property name="username" value="dev_user"/> <property name="pas

  • MyBatis传入集合 list 数组 map参数的写法

    foreach的主要用在构建in条件中,它可以在SQL语句中进行迭代一个集合.foreach元素的属性主要有item,index,collection,open,separator,close.item表示集合中每一个元素进行迭代时的别名,index指定一个名字,用于表示在迭代过程中,每次迭代到的位置,open表示该语句以什么开始,separator表示在每次进行迭代之间以什么符号作为分隔符,close表示以什么结束,在使用foreach的时候最关键的也是最容易出错的就是collection属性

  • Mybatis多参数及实体对象传递实例讲解

    在使用Mybatis的时候,经常会有各种各样的参数传递,不同类型,不同个数的参数. 先上个例子: public List<LifetouchRelease> findOfficeList(@Param("lifetouchRelease") LifetouchRelease lifetouchRelease, @Param("advertisementId") String advertisementId, @Param("officeName

  • Mybatis传递多个参数进行SQL查询的用法

    PS:ibatis3如何传递多个参数有两个方法:一种是使用java.Map,另一种是使用JavaBean. 当只向xxxMapper.xml文件中传递一个参数时,可以简单的用"_parameter"来接收xxxMapper.java传递进来的参数,并代入查询,比如说这样: (1)xxxMapper.java文件中这样定义: List<String> selectAllAirportCode(Boolean mapping); (2)这时在对应的xxxMapper.xml文件

  • MyBatis 参数类型为String时常见问题及解决方法

    1. 参数为String时的插值问题 假设有下面一Dao接口方法 public Account findByAccountType (String type)throws DaoException; 对应的Mapper.xml <select id="findByAccountType " parameterType="string" resultType="account"> select * form account <wh

  • Mybatis传递多个参数的解决办法(三种)

    小编给大家分享三种方案解决mybatis传递多个参数的问题,具体介绍如下所示: 第一种方案 DAO层的函数方法 Public User selectUser(String name,String area); 对应的Mapper.xml <select id="selectUser" resultMap="BaseResultMap"> select * from user_user_t where user_name = #{0} and user_a

  • MyBatis拦截器:给参数对象属性赋值的实例

    该拦截器的作用:在进行增加.修改等操作时,给数据模型的一些通用操作属性(如:创建人.创建时间.修改人.修改时间等)自动赋值. 该实现是在DAO层拦截,即存入DB前最后一层.后经分析,不是很合理,改为在service层拦截,用spring AOP来实现了,该代码遂弃用.不过已经测试可用,记录备忘. package com.development; import java.lang.reflect.InvocationTargetException; import java.util.Date; i

  • MyBatis传入参数的实例代码

    在MyBatis的select.insert.update.delete这些元素中都提到了parameterType这个属性.MyBatis现在可以使用的parameterType有基本数据类型和JAVA复杂数据类型 基本数据类型:包含int,String,Date等.基本数据类型作为传参,只能传入一个.通过#{参数名} 即可获取传入的值 复杂数据类型:包含JAVA实体类.Map.通过#{属性名}或#{map的KeyName}即可获取传入的值 基本数据类型参数示例: 根据班级ID查询教师列表 x

  • 解析Mybatis连续传递多个参数的方法

    MyBatis是一个支持普通SQL查询,存储过程和高级映射的优秀持久层框架.MyBatis消除了几乎所有的JDBC代码和参数的手工设置以及对结果集的检索封装.MyBatis可以使用简单的XML或注解用于配置和原始映射,将接口和Java的POJO(Plain Old Java Objects,普通的Java对象)映射成数据库中的记录. 下面给大家介绍Mybatis连续传递多个参数的方法.具体代码如下所示: // order by 排序 //<![CDATA[ 值 ]]> 过滤符号 //${}方式

随机推荐