好得很程序员自学网

<tfoot draggable='sEl'></tfoot>

深入了解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会首先经过下面方法来转换参数:

?

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

18

19

20

21

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指定了名字,就会记录这个名字,如果没有记录,那么就会使用它的序号作为名字。

例如有如下接口:

?

1

list<user> select( @param ( 'sex' )string sex,integer age);

那么他对应的params如下:

?

1

2

3

4

{

   0 : 'sex' ,

   1 : '1'

}

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

入参为null或没有时,参数转换为null 没有使用@param注解并且只有一个参数时,返回这一个参数 使用了@param注解或有多个参数时,将参数转换为map1类型,并且还根据参数顺序存储了key为param1,param2的参数。

注意:从第3种情况来看,建议各位有多个入参的时候通过@param指定参数名,方便后面(动态sql)的使用。

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

?

1

2

object param = method.convertargstosqlcommandparam(args);

result = sqlsession.<e>selectlist(command.getname(), param);

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

处理集合

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

在defaultsqlsession.java中的wrapcollection方法:

?

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

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中,从构造方法看起:

?

1

2

3

4

5

6

7

8

9

10

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类,这是一个内部静态类,代码如下:

?

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

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之后还有两行:

?

1

2

bindings.put(parameter_object_key, parameterobject);

bindings.put(database_id_key, configuration.getdatabaseid());

其中两个key分别为:

?

1

2

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:

?

1

2

3

static {

  ognlruntime.setpropertyaccessor(contextmap. class , new contextaccessor());

}

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

?

1

2

3

4

5

6

7

8

9

10

11

12

13

14

@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类型:

这种类型经过了下面的处理:

?

1

2

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方法:

?

1

2

3

4

5

6

7

8

9

10

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中取该属性的值,如果不存在,那么一定会执行到下面这行代码:

?

1

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方法:

?

1

2

3

4

5

6

7

8

9

10

11

12

13

@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内容为:

?

1

2

3

<select id= "selectorderby" resulttype= "user" >

select * from user order by ${value}

</select>

这种情况下,虽然没有指定一个value属性,但是mybatis会自动把参数column赋值进去。

再往下的代码:

?

1

2

object value = ognlcache.getvalue(content, context.getbindings());

string srtvalue = (value == null ? "" : string.valueof(value));

这里和动态sql就一样了,通过ognl方式来获取值。

看到这里使用ognl这种方式时,你有没有别的想法?

特殊用法:你是否在sql查询中使用过某些固定的码值?一旦码值改变的时候需要改动很多地方,但是你又不想把码值作为参数传进来,怎么解决呢?你可能已经明白了。

就是通过ognl的方式,例如有如下一个码值类:

?

1

2

3

4

5

package com.abel533.mybatis;

public interface code{

   public static final string enable = "1" ;

   public static final string disable = "0" ;

}

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

?

1

2

3

<select id= "selectuser" resulttype= "user" >

   select * from user where enable = ${ @com .abel533.mybatis.code @enable }

</select>

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

#{propertyname}参数

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

在defaultparameterhandler.java中:

?

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

18

19

20

21

22

23

24

25

26

27

28

29

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判断的地方,单独拿出来分析:

?

1

2

3

4

5

6

7

8

9

10

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中:

?

1

2

3

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>似乎是遇到问题最多的一个。

例如有下面的方法:

?

1

2

3

4

5

6

7

<insert id= "insertuserlist" >

  insert into user(username,password)

  values

  <foreach collection= "userlist" item= "user" separator= "," >

   (#{user.username},#{user.password})

  </foreach>

</insert>

对应的接口:

?

1

int insertuserlist( @param ( "userlist" )list<user> list);

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

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

?

1

2

map<string, object> bindings = context.getbindings();

final iterable<?> iterable = evaluator.evaluateiterable(collectionexpression, bindings);

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

?

1

2

3

4

5

6

7

{

  "_parameter" :{

   "param1" :list,

   "userlist" :list

  },

  "_databaseid" : null ,

}

collectionexpression就是collection="userlist"的值userlist。

我们看看evaluator.evaluateiterable如何处理这个参数,在expressionevaluator.java中的evaluateiterable方法:

?

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

18

19

20

21

22

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." );

}

首先通过看第一行代码:

?

1

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循环的时候,有一段需要关注的代码:

?

1

2

3

4

5

6

7

8

9

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)。

最后

这篇文章很长,写这篇文章耗费的时间也很长,超过10小时,写到半夜两点都没写完。

这篇文章真的非常有用,如果你对mybatis有一定的了解,这篇文章几乎是必读的一篇。

总结

以上就是这篇文章的全部内容了,希望本文的内容对大家的学习或者工作具有一定的参考学习价值,谢谢大家对的支持。如果你想了解更多相关内容请查看下面相关链接

原文链接:https://blog.csdn.net/isea533/article/details/44002219

查看更多关于深入了解MyBatis参数的详细内容...

  阅读:11次