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:
这里特别需要注意的一个地方是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的参数问题就不是问题了。