概述
我先说一下参数处理器的大概思路,然后再具体分析源码。上一节我们提到可以从SqlSource中获取到BoundSql,而BoundSql经过参数处理器设置参数后就能直接运行
BoundSql即解析完成的sql,对应的sql语句只会含有?,因此设置参数后就可以直接执行,那他是怎么设置参数的呢?举2个例子
如下sql封装成的BoundSql如图所示
<select id="selectByIds" resultType="org.apache.ibatis.mytest.UserInfo">
SELECT
<include refid="Base_Column_List"/>
FROM user_info WHERE id in
<foreach collection="list" open="(" close=")" separator="," item="item">
#{item}
</foreach>
</select>
从图中你可以看到BoundSql中的sql属性只会含有?,因此只需要给对应的位置设值即可
这就要用到parameterMappings和additionalParameters,依次从ParameterMapping中获取property属性作为key到additionalParameters这个Map中去拿值,然后给sql中?占位符的位置赋值即可
可以看到参数的映射关系是放在additionalParameters中
如下sql封装成的BoundSql如图所示
@Select("SELECT * FROM user_info WHERE name = #{name} and age = #{age}")
UserInfo selectByNameAndAge(@Param("name") String name, @Param("age") Integer age);
实现思路和上面类似,只不过这次是从parameterObject这个对象中根据属性去获取值了。
一般我们传入的是数组,list对象时,需要用foreache遍历,会把参数的映射关系放在additionalParameters,而其他情况则会将参数的映射关系放在parameterObject中
知道了大概思路,我们来看具体实现
iBATIS的参数处理方式
// 参数为1个时
UserInfo userInfo = sqlSession.selectOne("org.apache.ibatis.mytest.UserInfoMapper.selectById", 1);
// 参数为多个时封装成map
Map<String, Object> param = new HashMap<>();
param.put("name", "2");
param.put("age", 2);
UserInfo userInfo = sqlSession.selectOne("org.apache.ibatis.mytest.UserInfoMapper.selectByNameAndAge", param);
SimpleExecutor执行sql
org.apache.ibatis.executor.SimpleExecutor#doQuery
org.apache.ibatis.scripting.defaults.DefaultParameterHandler#setParameters
这个方法包含了所有参数处理器设置对象的逻辑,传入的参数种类比较多我们一个一个分析
boundSql.hasAdditionalParameter(propertyName)
先尝试从additionalParameters根据key获取值(这种是针对sql中有foreach标签的情况哈)
// 当参数为null,直接将?对应位置的值设为null即可
parameterObject == null
// 传入的参数能被TypeHandler处理,将?对应位置的值设为传入的值即可
typeHandlerRegistry.hasTypeHandler(parameterObject.getClass())
MetaObject metaObject = configuration.newMetaObject(parameterObject);
value = metaObject.getValue(propertyName);
这部分的情况有点复杂,传入的参数可能是一个Map,也可能是一个TypeHandler直接转换不了的对象,但不管是哪种情况,MethodObject这个工具类都能根据对应的属性获取值,这个工具类屏蔽了对这两种对象处理方式的差异
传入的是map
// java代码
Map<String, Object> param = new HashMap<>();
param.put("name", "2");
param.put("age", 2);
UserInfo userInfo = sqlSession.selectOne("org.apache.ibatis.mytest.UserInfoMapper.selectByNameAndAge", param);
// 对应的查询语句
// 当然如果是ibatis这种查询方式,@Param注解并没有任何作用,因为并没有解析@Param中的内容
@Select("SELECT * FROM user_info WHERE name = #{name} and age = #{age}")
UserInfo selectByNameAndAge(@Param("name") String name, @Param("age") Integer age);
可以看到不管map的value是普通的对象,还是用户自定义的对象,都能获取到值
传入的是用户自己定义的对象
// java代码
UserQuery query = new UserQuery();
query.setName("1");
query.setAge(1);
Object object = sqlSession.selectList("org.apache.ibatis.mytest.UserInfoMapper.selectByQuery", query);
// 对应的查询语句,mapper接口和xml
List<UserInfo> selectByQuery(UserQuery userQuery);
<select id="selectByQuery" resultType="org.apache.ibatis.mytest.UserInfo">
select id, name, age
from user_info
<where>
<if test="name != null and name != ''">
name = #{name}
</if>
<if test="age != null">
and age = #{age}
</if>
</where>
</select>
所以你看mybatis之所以能支持这么多传入参数的形式,MetaObject绝对功不可没
接着TypeHandler给sql中?占位符的位置赋值
当你对参数没有设置对应的TypeHandler时,会设置TypeHandler为UnknownTypeHandler,
UnknownTypeHandler会根据javaType和jdbcType选取合适的TypeHandler来进行赋值操作
TypeHandler
UnknownTypeHandler会根据javaType和jdbcType选取合适的TypeHandler来进行赋值操作
org.apache.ibatis.type.UnknownTypeHandler#setNonNullParameter
TypeHandlerRegistry构造函数中会初始化常见的映射关系
如果觉得系统提供的TypeHandler不能满足要求,你可以实现TypeHandler来定义javaType和jdbcType之间的转换逻辑。
例如,当java8出了新的时间api,LocalDate,LocalDateTime时,低版本的mybatis并不支持,此时我们就可以手动实现转换逻辑,然后配置到mybatis中
我们常用的TypeHandler基本上都继承了BaseTypeHandler,这里只是对设置的值为null时,做了统一的处理,不为null时则交给具体的TypeHandler来处理
具体的TypeHandler的实现非常简单,看下图
Mybatis的参数处理方式
UserInfoMapper mapper = sqlSession.getMapper(UserInfoMapper.class);
UserInfo userInfo = mapper.selectByIdAndAge(1, 1);
到了mybatis时代,使用Mapper接口的方式来执行sql时,会调用到MapperMethod#execute方法,因为通过Mapper接口调用时,有可能传入多个参数,而SqlSession执行sql时只支持单个参数,所以我们要通过执行method.convertArgsToSqlCommandParam(args)将多个参数合并为一个参数,那么合并的逻辑是怎样的?
在MapperMethod构造上中创建MethodSignature的时候,会对每个方法创建一个ParamNameResolver
构造函数主要作用就是构造names,保存参数位置和参数名称的映射关系,后续会用。分为两种情况加了@Param注解,和没有加@Param注解
@Param注解用mapper接口调用的方式的才生效,直接通过sqlsession调用的方式并没有任何作用,因为都没有解析
用了@Param注解
@Select("SELECT * FROM user_info WHERE name = #{name} and age = #{age}")
UserInfo selectByNameAndAge(@Param("name") String name, @Param("age") Integer age);
没有用@Paran注解
@Select("SELECT * FROM user_info WHERE id = #{id} and age = #{age}")
UserInfo selectByIdAndAge(Integer id, Integer age);
实际执行的时候调用convertArgsToSqlCommandParam方法将多个参数合并为一个参数
MapperMethod.MethodSignature#convertArgsToSqlCommandParam
可以看到主要分为3个部分
// 没有参数,直接返回null
args == null || paramCount == 0
// 没有@Param注解,并且只有一个入参(去掉了RowBounds和ResultHandler哈),则返回对应的入参即可
!hasParamAnnotation && paramCount == 1
当只有一个入参,且是集合类型时,做了一些特殊的处理
逻辑比较简单,就是给参数多起了一些别名,然后封装成一个map返回
因为map的key有arg0,collection,list,所以循环语句有如下3种写法
List<UserInfo> selectByIds(List<Integer> ids);
// 第一种写法
<foreach collection="arg0" open="(" close=")" separator="," item="item">
// 第二种写法
<foreach collection="collection" open="(" close=")" separator="," item="item">
// 第三种写法
<foreach collection="list" open="(" close=")" separator="," item="item">
接着就是最后一种情况,用了@Param注解或者有多个参数,封装成map返回,map的key为参数的名字,map的value为参数对应的值
UserInfo selectByNameAndAge(@Param("name") String name, @Param("age") Integer age);
// 第一种写法
@Select("SELECT * FROM user_info WHERE name = #{name} and age = #{age}")
// 第二种写法
@Select("SELECT * FROM user_info WHERE name = #{param1} and age = #{param2")
UserInfo selectByIdAndAge(Integer id, Integer age);
// 第一种写法
@Select("SELECT * FROM user_info WHERE id = #{arg0} and age = #{arg1}")
// 第二种写法
@Select("SELECT * FROM user_info WHERE id = #{param1} and age = #{param2}")