Mybatis 之参数问题

1.mybatis 是怎样处理参数的

1.1 mybatis的两种调用方式

1.1.1 mybatis 不依赖于接口通过sqlSession直接通过命名空间调用

这里写图片描述

 @Test
    public void testDySelect(){
        /**
         * 设置查询参数
         */
        Employee employee = new Employee();
        employee.setName("xiaohong");
        Department department = new Department();
        department.setId(2);
        employee.setDep(department);

        //获取sqlsession
        InputStream resourceAsStream = ConfigTest.class.getResourceAsStream("../classes/mybatis-config.xml");
        SqlSessionFactory sqlSessionFactory = new SqlSessionFactoryBuilder().build(resourceAsStream);
        SqlSession sqlSession = sqlSessionFactory.openSession();
        //sqlSession通过命名空间的方式直接调用
        sqlSession.selectList("com.worldly.config.mapper.EmployeeMapper.selectEmployeeList",employee);
        //通过接口的形式调用
        EmployeeMapper mapper = sqlSession.getMapper(EmployeeMapper.class);
        //mapper.selectEmployeeList(employee);

    }

执行结果:
这里写图片描述

1.1.2 mybatis 通过接口形式调用

   @Test
    public void testDySelect(){
        /**
         * 设置查询参数
         */
        Employee employee = new Employee();
        employee.setName("xiaohong");
        Department department = new Department();
        department.setId(2);
        employee.setDep(department);

        //获取sqlsession
        InputStream resourceAsStream = ConfigTest.class.getResourceAsStream("../classes/mybatis-config.xml");
        SqlSessionFactory sqlSessionFactory = new SqlSessionFactoryBuilder().build(resourceAsStream);
        SqlSession sqlSession = sqlSessionFactory.openSession();
        //sqlSession通过命名空间的方式直接调用
        //sqlSession.selectList("com.worldly.config.mapper.EmployeeMapper.selectEmployeeList",employee);
        //通过接口的形式调用
        EmployeeMapper mapper = sqlSession.getMapper(EmployeeMapper.class);
        mapper.selectEmployeeList(employee);

    }

接口调用的console
这里写图片描述

1.1.3 接口调用与命名空间调用参数是怎样处理的

通过对上述两种方式进行debug调试发现,接口调用 和命名空间调用大部分都是相同的。接口调用的时序图
这里写图片描述
以下面接口为例 debug

 Employee selectEmployeeByCondition2(@Param("id")Integer id,@Param("name")String name,Integer depId);

接口调用,将调用convertArgsToSqlCommandParam对参数处理的源码

public Object convertArgsToSqlCommandParam(Object[] args) {
            int paramCount = this.params.size();
            //参数不为空 
            if(args != null && paramCount != 0) {
                //没有注解 并且是只有一个参数直接返回参数
                if(!this.hasNamedParameters && paramCount == 1) {
                    return args[((Integer)this.params.keySet().iterator().next()).intValue()];
                } else {
                    //有注解或者是多个参数的情况
                    Map<String, Object> param = new MapperMethod.ParamMap();
                    int i = 0;

                    for(Iterator i$ = this.params.entrySet().iterator(); i$.hasNext(); ++i) {
                        Entry<Integer, String> entry = (Entry)i$.next();
                        //此时的entry的 value为@Param("id")或者是该参数在参数列表中的下标
                        //所以这步存的是Map{id:args[0]}或者Map{2:args[2]}
                        param.put(entry.getValue(), args[((Integer)entry.getKey()).intValue()]);
                        String genericParamName = "param" + String.valueOf(i + 1);
                        if(!param.containsKey(genericParamName)) {
                            param.put(genericParamName, args[((Integer)entry.getKey()).intValue()]);
                        }
                    }

                    return param;
                }
            } else {
                //参数为空的情况 直接返回空
                return null;
            }
        }

此时的args是
这里写图片描述
在这里有个很关键的params,这个参数类型为Map<Integer, String>,他会根据接口方法按顺序记录下接口参数的定义的名字,如果使用@Param指定了名字,就会记录这个名字,如果没有记录,那么就会使用它的序号作为名字。
此刻params的参数值通过MapperMethod的构造函数进行赋值如下图:
这里写图片描述

经过时序图中的第4步操作 params 是如下的值:

{
    0:'id',
    1:'name',
    2:'2'
}

继续看上面的convertArgsToSqlCommandParam方法,这里
简要说明3种情况:
1. 入参为null或没有时,参数转换为null
2. 没有使用@Param注解并且只有一个参数时,返回这一个参数
3.使用了@Param注解或有多个参数时,将参数转换为Map1类型,并且还根据参数顺序存储了key为param1,param2的参数。

此刻接口调用方式Mybaits对参数进行封装处理后的结果 如下图:
这里写图片描述

至此 接口调用的前期部分的参数处理就完成了,然后调用命名空间部分的参数处理(即接口调用与命名空间调用共同部分)

public <E> List<E> selectList(String statement, Object parameter, RowBounds rowBounds) {
        List var5;
        try {
            //接口调用第一部分 封装接口参数得到ms
            MappedStatement ms = this.configuration.getMappedStatement(statement);
            //接口与命名空间调用的共同部分 this.wrapCollection(parameter)
            var5 = this.executor.query(ms, this.wrapCollection(parameter), rowBounds, Executor.NO_RESULT_HANDLER);
        } catch (Exception var9) {
            throw ExceptionFactory.wrapException("Error querying database.  Cause: " + var9, var9);
        } finally {
            ErrorContext.instance().reset();
        }

        return var5;
    }

此刻selectList方法的两个参数值 如下debug图:
这里写图片描述
共同参数处理部分 wrapCollection(parameter)时序图:
这里写图片描述
通过时序图中的5步对参数进行处理

private Object wrapCollection(Object object) {
        DefaultSqlSession.StrictMap map;
        if(object instanceof Collection) {
            map = new DefaultSqlSession.StrictMap();
            map.put("collection", object);
            if(object instanceof List) {
                map.put("list", object);
            }

            return map;
        } else if(object != null && object.getClass().isArray()) {
            map = new DefaultSqlSession.StrictMap();
            map.put("array", object);
            return map;
        } else {
            return object;
        }
    }

此刻wrapCollection 的返回结果只如下debug:
这里写图片描述

扫描二维码关注公众号,回复: 1518264 查看本文章

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

2.我们在项目中怎样使用参数

我们可能会在以下几种情况用到参数

2.1 在sql语句中取值

select * from t_emp WHERE emp_id=${id} and emp_name=#{name}

根据上述的参数封装过程我们可以得出结论:
${id}来取得id的值,也可以用 ${param1} 来取id的值

2.2 动态sql中(包括条件判断以及取值)

通常我们在动态 sql中用到参数都是要取得其参数值。如<bind> 标签取到参数值 赋值给一个全局参数;<if> 标签取到参数值进行条件判断;<foreach>标签取得集合或者数组参数值然后进行遍历。那我们动态sql类会将这些参数进行封装么?

传到这步的参数经过MapperMethod.MethodSignature.convertArgsToSqlCommandParam(args)和DefaultSqlsession.wrapCollection(parameter)封装后的参数。此时的参数类型有下面三种情况:

1. null类型的参数
2. 不是null的Map类型参数。
3. array,list,collection和Map以外的Object类型参数。

动态sql会对上面3种参数进行进一步封装,这一过程是通过生成DynamicContext 对象来完成的。
处理动态sql参数的时序图
这里写图片描述
代码如下:

public DynamicContext(Configuration configuration, Object parameterObject) {
        /**
         * 1.参数为null
         * 2.非Map类型Object
         */
        if(parameterObject != null && !(parameterObject instanceof Map)) {
            MetaObject metaObject = configuration.newMetaObject(parameterObject);
            //将这些非map参数在 bindings对象存一份
            this.bindings = new DynamicContext.ContextMap(metaObject);
        } else {
        /**
        * 1.本身参数就是map类型的
        * 2.本身参数是list,array,collection(DefaultSqlsession.wrapCollection封装map类型的)
        * 
        * 这些Map参数在bindings本身是不会存值。
        */
            this.bindings = new DynamicContext.ContextMap((MetaObject)null);
        }

        //最后参数都会放在key为_parameter中存一份值
        this.bindings.put("_parameter", parameterObject);
        this.bindings.put("_databaseId", configuration.getDatabaseId());
    }

其中DynamicContext.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;
  }
}

看上面构造方法,如果参数是1,2情况时,执行代码else中的代码;参数是3情况时执行if中的代码。也就是说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的时候,此时Object result=map.get(name);得到result=null,继续走Object parameterObject=map.get(PARAMETER_OBJECT_KEY);中parameterObject=null。因此最后返回的结果都是null(name=”_databaseId”除外,可能会有值)。此时不过name属性存不存在都不会报错。
2 非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());

3 array,list,collection和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 中的参数的处理过程,虽然分了 null ,map 以及其他情况处理但是都在 key 为”_parameter”的map中有一份值如下图:

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

因此我们只要通过OGNL表达

  • 如果 我们传的参数是一个Object 那么 直接用_parameter.attr 可以多级属性点访问来获取Object的attr。
  • 如果我们传的参数是一个List,Array,Collection那么可以用_parameter.list/array/collection.[‘i’].attr 来遍历每个对象的属性。
  • 如果我们传的参数是Map,那么我们可以用_parameter.get(key).attr来比例对象的属性。

3.参数处理以及取值的总结

3.1 普通参数处理及取值

接口调用方式先将接口中的参数按照如下规则进行处理:
MapperMethod 构造方法会将所有的参数按是否有@Param和没有的两种情况进行处理。然后得到很关键的params,这个参数类型为Map<Integer, String>

1. 如果是用@Param注解的直接用 该注解的value
2. 如果是没有的会就使用该参数在参数列表中的 数组索引来知道。

通过上述两种方法可以得到params 如我们上面的接口

Employee selectEmployeeByCondition2(@Param("id")Integer id,@Param("name")String name,Integer depId);
{
    0:'id',
    1:'name',
    2:'2'
}

然后通过参数处理可以得到最终param。
这里写图片描述
他是mapper类型的里面key 可能为 @Param的value值也可以为param1

因此普通sql中我们可以通过 param1 或者 @Param的vaule值 或者参数在参数列表中对应得下标来取得。
如下

<!--分别是param1, 注解值,参数下标-->
select * from t_emp WHERE emp_id=${param1} and emp_name=#{name} and emp_dep=#{2}

3.2 动态参数处理及取值

这里讲的动态参数取值只是在<if>,<foreach>,<when>,<bind>等的test后面的参数,其他的地方依旧是普通参数取值。
经 2分析源码

this.bindings.put("_parameter", parameterObject);

我们可以知道动态sql参数就是将普通的sql参数进行用key为”_parameter” value为 “所传param” 的map。再次封装。
因此取值的时候通过ognl表达式 按一下方式取值

  • 如果 我们传的参数是一个Object 那么 直接用_parameter.attr 可以多级属性点访问来获取Object的attr。
  • 如果我们传的参数是一个List,Array,Collection那么可以用_parameter.list/array/collection.[‘i’].attr 来遍历每个对象的属性。
  • 如果我们传的参数是Map,那么我们可以用_parameter.get(key).attr来比例对象的属性。

此篇博客经过两次编写才完成,内容有点多,仔细阅读不难理解。如果理解了,个人觉得mybatis的参数问题就不是问题了。

猜你喜欢

转载自blog.csdn.net/u014297148/article/details/79688398