Mybatis源码解析:参数处理器是如何兼容这么多种类型的参数?

在这里插入图片描述

概述

我先说一下参数处理器的大概思路,然后再具体分析源码。上一节我们提到可以从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}")

参考博客

  • 2
    点赞
  • 5
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

Java识堂

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值