概述
我先說一下參數處理器的大概思路,然後再具體分析源碼。上一節我們提到可以從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}")