zoukankan      html  css  js  c++  java
  • 深入了解MyBatis参数

    深入了解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);<span></span>

    那么他对应的params如下:

    1
    2
    3
    4
    {
        0:'sex',
        1:'1'
    }

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

    1. 入参为null或没有时,参数转换为null
    2. 没有使用@Param 注解并且只有一个参数时,返回这一个参数
    3. 使用了@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>中默认情况下写的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中,从构造方法看起:

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

    1. null,如果没有入参或者入参是null,到这里也是null。
    2. Map类型,除了null之外,前面两步主要是封装成Map类型。
    3. 数组、集合和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
    15
    16
    17
    @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方法:

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

    <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的方式,例如有如下一个码值类:

    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类型,通过反射方便的取值。

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

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

    例如有下面的方法:

    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>

    对应的接口:

    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的值分四种情况。

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

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

    最后

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

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

    如果各位发现文中错误或者其他问题欢迎留言或加群详谈。

    MyBatis分页插件

    http://git.oschina.net/free/Mybatis_PageHelper

    MyBatis通用Mapper

    http://git.oschina.net/free/Mapper

    Mybatis专栏:


    1. 这里的Map实际类型为ParamMap<V>,和下一步处理集合中的StrictMap<V>类是两个功能完全一样的类。
    2. 这里的Map实际类型为StrictMap<V>,和接口处理中的ParamMap<V>类是两个功能完全一样的类。
  • 相关阅读:
    Nginx 部署多个 web 项目(虚拟主机)
    Nginx 配置文件
    Linux 安装 nginx
    Linux 安装 tomcat
    Linux 安装 Mysql 5.7.23
    Linux 安装 jdk8
    Linux 安装 lrzsz,使用 rz、sz 上传下载文件
    springMVC 拦截器
    spring 事务
    基于Aspectj 注解实现 spring AOP
  • 原文地址:https://www.cnblogs.com/shsgl/p/4999645.html
Copyright © 2011-2022 走看看