天天看點

Mybatis源碼解析:參數處理器是如何相容這麼多種類型的參數?

Mybatis源碼解析:參數處理器是如何相容這麼多種類型的參數?

概述

我先說一下參數處理器的大概思路,然後再具體分析源碼。上一節我們提到可以從SqlSource中擷取到BoundSql,而BoundSql經過參數處理器設定參數後就能直接運作

Mybatis源碼解析:參數處理器是如何相容這麼多種類型的參數?

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>      
Mybatis源碼解析:參數處理器是如何相容這麼多種類型的參數?

從圖中你可以看到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);      
Mybatis源碼解析:參數處理器是如何相容這麼多種類型的參數?

實作思路和上面類似,隻不過這次是從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

Mybatis源碼解析:參數處理器是如何相容這麼多種類型的參數?
Mybatis源碼解析:參數處理器是如何相容這麼多種類型的參數?

org.apache.ibatis.scripting.defaults.DefaultParameterHandler#setParameters

Mybatis源碼解析:參數處理器是如何相容這麼多種類型的參數?

這個方法包含了所有參數處理器設定對象的邏輯,傳入的參數種類比較多我們一個一個分析

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);      
Mybatis源碼解析:參數處理器是如何相容這麼多種類型的參數?

可以看到不管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源碼解析:參數處理器是如何相容這麼多種類型的參數?

是以你看mybatis之是以能支援這麼多傳入參數的形式,MetaObject絕對功不可沒

接着TypeHandler給sql中?占位符的位置指派

當你對參數沒有設定對應的TypeHandler時,會設定TypeHandler為UnknownTypeHandler,

UnknownTypeHandler會根據javaType和jdbcType選取合适的TypeHandler來進行指派操作

TypeHandler

Mybatis源碼解析:參數處理器是如何相容這麼多種類型的參數?

UnknownTypeHandler會根據javaType和jdbcType選取合适的TypeHandler來進行指派操作

org.apache.ibatis.type.UnknownTypeHandler#setNonNullParameter

Mybatis源碼解析:參數處理器是如何相容這麼多種類型的參數?

TypeHandlerRegistry構造函數中會初始化常見的映射關系

Mybatis源碼解析:參數處理器是如何相容這麼多種類型的參數?

如果覺得系統提供的TypeHandler不能滿足要求,你可以實作TypeHandler來定義javaType和jdbcType之間的轉換邏輯。例如,當java8出了新的時間api,LocalDate,LocalDateTime時,低版本的mybatis并不支援,此時我們就可以手動實作轉換邏輯,然後配置到mybatis中

Mybatis源碼解析:參數處理器是如何相容這麼多種類型的參數?

我們常用的TypeHandler基本上都繼承了BaseTypeHandler,這裡隻是對設定的值為null時,做了統一的處理,不為null時則交給具體的TypeHandler來處理

Mybatis源碼解析:參數處理器是如何相容這麼多種類型的參數?

具體的TypeHandler的實作非常簡單,看下圖

Mybatis源碼解析:參數處理器是如何相容這麼多種類型的參數?

Mybatis的參數處理方式

UserInfoMapper mapper = sqlSession.getMapper(UserInfoMapper.class);
UserInfo userInfo = mapper.selectByIdAndAge(1, 1);      

到了mybatis時代,使用Mapper接口的方式來執行sql時,會調用到MapperMethod#execute方法,因為通過Mapper接口調用時,有可能傳入多個參數,而SqlSession執行sql時隻支援單個參數,是以我們要通過執行method.convertArgsToSqlCommandParam(args)将多個參數合并為一個參數,那麼合并的邏輯是怎樣的?

Mybatis源碼解析:參數處理器是如何相容這麼多種類型的參數?

在MapperMethod構造上中建立MethodSignature的時候,會對每個方法建立一個ParamNameResolver

Mybatis源碼解析:參數處理器是如何相容這麼多種類型的參數?

構造函數主要作用就是構造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);      
Mybatis源碼解析:參數處理器是如何相容這麼多種類型的參數?

沒有用@Paran注解

@Select("SELECT * FROM user_info WHERE id = #{id} and age = #{age}")
UserInfo selectByIdAndAge(Integer id, Integer age);      
Mybatis源碼解析:參數處理器是如何相容這麼多種類型的參數?

實際執行的時候調用convertArgsToSqlCommandParam方法将多個參數合并為一個參數MapperMethod.MethodSignature#convertArgsToSqlCommandParam

Mybatis源碼解析:參數處理器是如何相容這麼多種類型的參數?
Mybatis源碼解析:參數處理器是如何相容這麼多種類型的參數?

可以看到主要分為3個部分

// 沒有參數,直接傳回null
args == null || paramCount == 0      
// 沒有@Param注解,并且隻有一個入參(去掉了RowBounds和ResultHandler哈),則傳回對應的入參即可
!hasParamAnnotation && paramCount == 1      

當隻有一個入參,且是集合類型時,做了一些特殊的處理

Mybatis源碼解析:參數處理器是如何相容這麼多種類型的參數?

邏輯比較簡單,就是給參數多起了一些别名,然後封裝成一個map傳回

Mybatis源碼解析:參數處理器是如何相容這麼多種類型的參數?

因為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}")      

參考部落格