文章目錄
- 一、前言
- 二、TypeHandler
- 三、KeyGenerator
- 四、Plugin
- 1 Interceptor
- 2 org.apache.ibatis.plugin.Plugin
- 3. 調用場景
- 五、Mybatis 嵌套映射 BUG
- 1. 示例
- 2. 原因
- 3. 解決方案
- 六、discriminator 標簽
- 七、其他
- 1. RowBounds
- 2. ResultHandler
- 3. @MapKey
一、前言
Mybatis 官網 以及 本系列文章地址:
- Mybatis 源碼 ① :開篇
- Mybatis 源碼 ② :流程分析
- Mybatis 源碼 ③ :SqlSession
- Mybatis 源碼 ④ :TypeHandler
- Mybatis 源碼 ∞ :雜七雜八
主要是 Mybatis 的一些雜七雜八的內容,用于自己可以快速定位一些問題,所以部分內容寫比較隨性
二、TypeHandler
關于 TypeHandler 的使用,各處都是文章,這里就不再貼出完整的項目,僅對關鍵內容進行說明。
- 注冊或聲明 TypeHandler :
-
通過 mybatis.type-handlers-package 直接指定包路徑 :該路徑下的 TypeHandler 實現類都會被自動注冊,并且只要是符合轉換類型無論是入參還是出參都會經過轉換
mybatis.type-handlers-package=com.kingfish.config.handler
-
Xml 中 通過如下標簽注冊,可以指定注冊哪些 TypeHandler,并且只要是符合轉換類型無論是入參還是出參都會經過轉換。
<configuration><typeHandlers><typeHandler handler="com.kingfish.config.handler.PwdTypeHandler"/></typeHandlers> </configuration>
-
<result>
標簽 通過 typeHandler 屬性指定,指定某個屬性使用 TypeHandler 查詢,需要注意的是,僅僅是返回類型是當前 ResultMap 時才會進行類型轉換:<resultMap id="BaseResultMap" type="com.kingfish.entity.SysUser"><result property="password" column="password" jdbcType="VARCHAR" typeHandler="com.kingfish.config.handler.PwdTypeHandler"/> </resultMap>
-
-
定義密碼加解密類型轉換器 : PwdTypeHandler。密碼不能明文存儲在庫中,所以當我們需要對DB 中的密碼進行加密處理。這里便可以通過 TypeHandler 來實現(在新增、更新、刪除時自動加密,在查詢時自動解密)
public class PwdTypeHandler extends BaseTypeHandler<String> {private static final SymmetricCrypto AES = new SymmetricCrypto(SymmetricAlgorithm.AES, "1234567890123456".getBytes());@Overridepublic void setNonNullParameter(PreparedStatement ps, int i, String parameter, JdbcType jdbcType) throws SQLException {ps.setString(i, AES.encryptBase64(parameter));}@Overridepublic String getNullableResult(ResultSet rs, String columnName) throws SQLException {return AES.decryptStr(rs.getString(columnName));}@Overridepublic String getNullableResult(ResultSet rs, int columnIndex) throws SQLException {return AES.decryptStr(rs.getString(columnIndex));}@Overridepublic String getNullableResult(CallableStatement cs, int columnIndex) throws SQLException {return AES.decryptStr(cs.getString(columnIndex));} }
需要注意的是
-
如果以注冊的方式(mybatis.type-handlers-package 或者
<typeHandlers>
標簽)注冊該 TypeHandler。只要是符合其類型轉換的情況都會使用該處理器轉化,如上面 PwdTypeHandler 轉換類型是 String,即只要字段類型是 String,都會被該處理器處理,比如 user_name 也是 String 類型,入庫后也會被加密。這種情況并非我們想要的。所以我們可以通過自定義復雜類型的方式來避免將其他類型轉換,或者通過下面 標簽屬性 的方式來轉換。 -
如果是通過 標簽的 typeHandler 屬性指定,則只會在查詢返回結果時對指定結果集中的指定字段進行處理。
<!-- 返回轉換(忽略了其他字段) --><resultMap id="BaseResultMap" type="com.kingfish.entity.SysUser"><result property="password" column="password" jdbcType="VARCHAR" typeHandler="com.kingfish.config.handler.PwdTypeHandler"/></resultMap><!-- 插入轉換 --> <insert id="insert" keyProperty="id" useGeneratedKeys="true" >insert into sys_user(create_time, modify_time, user_name, password, status, is_delete, nick_name, phone, extend)values (#{createTime}, #{modifyTime}, #{userName}, #{password, typeHandler=com.kingfish.config.handler.PwdTypeHandler}, #{status}, #{isDelete}, #{nickName}, #{phone}, #{extend})</insert><!-- 更新轉換(忽略了其他字段)--><update id="update">update sys_user<set><if test="password != null and password != ''">password = #{password, typeHandler=com.kingfish.config.handler.PwdTypeHandler}</if></set>where id = #{id}</update>
三、KeyGenerator
在Mybatis中,執行insert操作時,如果我們希望返回數據庫生成的自增主鍵值,那么就需要使用到KeyGenerator對象。
關于 KeyGenerator 的內容,這里直接摘取 Mybatis之KeyGenerator 的部分內容,詳細部分請閱讀原文
KeyGenerator 定義如下:
public interface KeyGenerator {// BaseStatementHandler 構造函數中調用,在sql 執行前調用void processBefore(Executor executor, MappedStatement ms, Statement stmt, Object parameter);// StatementHandler#update 中會調用,在sql 執行后調用void processAfter(Executor executor, MappedStatement ms, Statement stmt, Object parameter);}
存在如下三個實現類:
- Jdbc3KeyGenerator:用于處理數據庫支持自增主鍵的情況,如MySQL的auto_increment。
- NoKeyGenerator:空實現,不需要處理主鍵。
- SelectKeyGenerator:用于處理數據庫不支持自增主鍵的情況,比如Oracle的sequence序列。
下面以 Jdbc3KeyGenerator 為例簡單看下
@Overridepublic void processAfter(Executor executor, MappedStatement ms, Statement stmt, Object parameter) {processBatch(ms, stmt, parameter);}public void processBatch(MappedStatement ms, Statement stmt, Object parameter) {// 獲取key屬性名,一般來說即 id,說明 key 就是屬性名為 id 的字段 final String[] keyProperties = ms.getKeyProperties();if (keyProperties == null || keyProperties.length == 0) {return;}try (ResultSet rs = stmt.getGeneratedKeys()) {final ResultSetMetaData rsmd = rs.getMetaData();final Configuration configuration = ms.getConfiguration();// 如果列的長度小于 key的長度則不處理if (rsmd.getColumnCount() < keyProperties.length) {// Error?} else {// 賦值keyassignKeys(configuration, rs, rsmd, keyProperties, parameter);}} catch (Exception e) {throw new ExecutorException("Error getting generated key or setting result to parameter object. Cause: " + e, e);}}private void assignKeys(Configuration configuration, ResultSet rs, ResultSetMetaData rsmd, String[] keyProperties,Object parameter) throws SQLException {if (parameter instanceof ParamMap || parameter instanceof StrictMap) {// Multi-param or single param with @Param// 多個參數或單一參數 使用 @Param 場景assignKeysToParamMap(configuration, rs, rsmd, keyProperties, (Map<String, ?>) parameter);} else if (parameter instanceof ArrayList && !((ArrayList<?>) parameter).isEmpty()&& ((ArrayList<?>) parameter).get(0) instanceof ParamMap) {// Multi-param or single param with @Param in batch operation// 多個參數或單一參數 使用 @Param 批量操作的場景assignKeysToParamMapList(configuration, rs, rsmd, keyProperties, (ArrayList<ParamMap<?>>) parameter);} else {// Single param without @Param// 單個參數未使用 @Param 的場景assignKeysToParam(configuration, rs, rsmd, keyProperties, parameter);}}
下面以單個參數未使用 @Param 場景為例
private void assignKeysToParam(Configuration configuration, ResultSet rs, ResultSetMetaData rsmd,String[] keyProperties, Object parameter) throws SQLException {// 將對象轉換為 集合,就是簡單封裝Collection<?> params = collectionize(parameter);if (params.isEmpty()) {return;}List<KeyAssigner> assignerList = new ArrayList<>();for (int i = 0; i < keyProperties.length; i++) {assignerList.add(new KeyAssigner(configuration, rsmd, i + 1, null, keyProperties[i]));}Iterator<?> iterator = params.iterator();// 遍歷參數while (rs.next()) {if (!iterator.hasNext()) {throw new ExecutorException(String.format(MSG_TOO_MANY_KEYS, params.size()));}// 獲取參數Object param = iterator.next();// 反射將Key 值映射到 參數對應的屬性上 (即將id的值映射到 param 的id 屬性上)assignerList.forEach(x -> x.assign(rs, param));}}
四、Plugin
Mybatis支持我們通過插件的方式擴展具體的過程,我們可以通過如下方式:
// 聲明當前類是個攔截器,攔截的類型是 StatementHandler,方法名是 prepare,該方法的入參 Connection 和 Integer 類型。
// 當 StatementHandler 的 prepare 方法執行時會被該攔截器攔截
@Component
@Intercepts({ @Signature(type = StatementHandler.class, method = "prepare", args = { Connection.class, Integer.class}) })
public class DemoPlugins implements Interceptor {@Overridepublic Object intercept(Invocation invocation) throws Throwable {System.out.println("invocation = " + invocation);return null;}
}
下面我們來看看代碼的具體實現
在上面我們提到負責執行Sql的 Executor 被 Interceptor 包裝了,實際上并非僅僅只有 執行器會被攔截器攔截,因此我們這里來看看 Mybatis 攔截器的具體實現。
如下是 InterceptorChain#pluginAll 的實現,當創建 Executor、ParameterHandler、ResultSetHandler、StatementHandler 時都會調用該方法:
public Object pluginAll(Object target) {for (Interceptor interceptor : interceptors) {target = interceptor.plugin(target);}return target;}
可以看到,該方法會通過 Interceptor#plugin 方法對 target 進行包裝,具體如下:
1 Interceptor
org.apache.ibatis.plugin.Interceptor 定義如下:
public interface Interceptor {Object intercept(Invocation invocation) throws Throwable;default Object plugin(Object target) {// 使用當前對象包裝 targetreturn Plugin.wrap(target, this);}// XML 解析 interceptor 時會調用該方法進行屬性賦值,具體看實現default void setProperties(Properties properties) {// NOP}}
這里可以看到,Mybatis 通過 Plugin#wrap 方法代理并返回了一個新的對象。下面我們來看下 org.apache.ibatis.plugin.Plugin 的具體實現。
2 org.apache.ibatis.plugin.Plugin
org.apache.ibatis.plugin.Plugin#wrap 實現如下:
public static Object wrap(Object target, Interceptor interceptor) {// 1. 獲取方法簽名Map<Class<?>, Set<Method>> signatureMap = getSignatureMap(interceptor);Class<?> type = target.getClass();// 獲取 type 的所有實現接口Class<?>[] interfaces = getAllInterfaces(type, signatureMap);if (interfaces.length > 0) {// 創建新的代理對象,這里看到,處理器實際上是Pluginreturn Proxy.newProxyInstance(type.getClassLoader(),interfaces,new Plugin(target, interceptor, signatureMap));}return target;}// 解析Intercepts注解并獲取方法簽名private static Map<Class<?>, Set<Method>> getSignatureMap(Interceptor interceptor) {// 獲取 @Intercepts 注解信息Intercepts interceptsAnnotation = interceptor.getClass().getAnnotation(Intercepts.class);// issue #251if (interceptsAnnotation == null) {throw new PluginException("No @Intercepts annotation was found in interceptor " + interceptor.getClass().getName());}// 獲取 @Intercepts 注解的 @Signature 簽名信息Signature[] sigs = interceptsAnnotation.value();Map<Class<?>, Set<Method>> signatureMap = new HashMap<>();for (Signature sig : sigs) {// 創建 代理方法集合,被代理的方法會保存到該 Set 中Set<Method> methods = MapUtil.computeIfAbsent(signatureMap, sig.type(), k -> new HashSet<>());try {// 獲取 @Signature.type 指定的類,方法名為 sig.method(),參數為 sig.args() 的方法Method method = sig.type().getMethod(sig.method(), sig.args());methods.add(method);} catch (NoSuchMethodException e) {throw new PluginException("Could not find method on " + sig.type() + " named " + sig.method() + ". Cause: " + e, e);}}// 返回代理方法簽名return signatureMap;}
可以看到,這里會為 target 創建一個代理對象,代理處理器由 Plugin 來擔任,Plugin#invoke 方法如下:
@Overridepublic Object invoke(Object proxy, Method method, Object[] args) throws Throwable {try {// 從代理方法簽名中獲取當前類的代理方法,如果當前方法需要代理則進行代理,否則執行調用Set<Method> methods = signatureMap.get(method.getDeclaringClass());if (methods != null && methods.contains(method)) {// 執行代理攔截器,這里 interceptor 實際上是 Interceptor 的實現類,也就是 Mybatis 的插件類return interceptor.intercept(new Invocation(target, method, args));}return method.invoke(target, args);} catch (Exception e) {throw ExceptionUtil.unwrapThrowable(e);}}
3. 調用場景
在 Mybatis 中,插件的包裝調用都在 Configuration 中,如下
public ParameterHandler newParameterHandler(MappedStatement mappedStatement, Object parameterObject, BoundSql boundSql) {ParameterHandler parameterHandler = mappedStatement.getLang().createParameterHandler(mappedStatement, parameterObject, boundSql);parameterHandler = (ParameterHandler) interceptorChain.pluginAll(parameterHandler);return parameterHandler;}public ResultSetHandler newResultSetHandler(Executor executor, MappedStatement mappedStatement, RowBounds rowBounds, ParameterHandler parameterHandler,ResultHandler resultHandler, BoundSql boundSql) {ResultSetHandler resultSetHandler = new DefaultResultSetHandler(executor, mappedStatement, parameterHandler, resultHandler, boundSql, rowBounds);resultSetHandler = (ResultSetHandler) interceptorChain.pluginAll(resultSetHandler);return resultSetHandler;}public StatementHandler newStatementHandler(Executor executor, MappedStatement mappedStatement, Object parameterObject, RowBounds rowBounds, ResultHandler resultHandler, BoundSql boundSql) {StatementHandler statementHandler = new RoutingStatementHandler(executor, mappedStatement, parameterObject, rowBounds, resultHandler, boundSql);statementHandler = (StatementHandler) interceptorChain.pluginAll(statementHandler);return statementHandler;}public Executor newExecutor(Transaction transaction, ExecutorType executorType) {executorType = executorType == null ? defaultExecutorType : executorType;executorType = executorType == null ? ExecutorType.SIMPLE : executorType;Executor executor;if (ExecutorType.BATCH == executorType) {executor = new BatchExecutor(this, transaction);} else if (ExecutorType.REUSE == executorType) {executor = new ReuseExecutor(this, transaction);} else {executor = new SimpleExecutor(this, transaction);}if (cacheEnabled) {executor = new CachingExecutor(executor);}executor = (Executor) interceptorChain.pluginAll(executor);return executor;}
這里可以看到Mybatis Plugin 的實現還是比較簡單的,通過注解解析,來創建對應類的對應方法的攔截器,(如 PageHelper 的實現核心就是通過 com.github.pagehelper.PageInterceptor 來完成的。)
五、Mybatis 嵌套映射 BUG
1. 示例
Mybatis 嵌套映射在行數據完全相同時 (這里的行數據完全相同指的是sql 查詢出來的數據萬完全相同,而非 Mybatis 的ResultMap 映射的字段的值完全相同)會丟失的缺陷,以下面為例子 :
-
sys_user 表數據如下
-
sys_role 數據如下
-
執行如下SQL, 該 Sql 目的是為了查詢有幾個用戶具有admin 權限,這里可以看到使用了Left join 所以會返回兩條完全相同的數據:
SELECTsr.*,su.user_name user_user_name,su.PASSWORD user_passwordFROMsys_role srLEFT JOIN sys_user su ON sr.id = su.role_idwhere sr.id = 1
執行結果如下:
-
但實際上如果通過Mybatis 執行上述邏輯則會出現錯誤結果如下:
SysRoleDto 如下,這里不再貼出SysUser:
public class SysRoleDto {/*** 自增主鍵ID*/private Long id;/*** 用戶名*/private String roleName;/*** 狀態*/private String status;/*** 用戶*/private List<SysUser> sysUsers; }
Mapper 如下:
<mapper namespace="com.kingfish.dao.SysRoleDao"><resultMap id="BaseResultMap" type="com.kingfish.entity.SysRole"><result property="id" column="id" jdbcType="INTEGER"/><result property="roleName" column="role_name" jdbcType="VARCHAR"/><result property="status" column="status" jdbcType="VARCHAR" /><!-- 忽略余下屬性 --></resultMap><!-- 內部嵌套映射 --><resultMap id="InnerNestMap" type="com.kingfish.entity.dto.SysRoleDto" extends="BaseResultMap"><!-- 指定 sysUsers 屬性都是前綴為 user_ 的屬性 --><collection property="sysUsers" columnPrefix="user_"resultMap="com.kingfish.dao.SysUserDao.BaseResultMap"></collection></resultMap><!-- 通過聯表查詢出來多個屬性,如果屬性名跟 sysUsers 對應的com.kingfish.dao.SysUserDao.BaseResultMap配置的屬性名一致則會映射上去 (屬性名映射規則受到columnPrefix影響) --><select id="selectRoleUser" resultMap="InnerNestMap">SELECTsr.*,su.user_name user_user_name,su.PASSWORD user_passwordFROMsys_role srLEFT JOIN sys_user su ON sr.id = su.role_idwhere sr.id = 1</select> </mapper>
-
執行結果如下,可以發現 sysUsers 屬性少了一條記錄,因為這里兩條查詢的記錄相同 在nestedResultObjects 中被判斷已經存在。
-
如果我們把其中一個【張三】改成【李四】,其余全都不動,那么sysUsers兩條記錄數據就不相同,則不會出現這種問題,如下:
執行結果如下:
2. 原因
該缺陷的原因在于在 Mybatis 中會緩存嵌套對象到 DefaultResultSetHandler#nestedResultObjects 中,而緩存的key 的生成策略可以簡單理解為 resultMapid + 屬性名 + 屬性值。而上面的例子中 Sql正常執行是如下數據,可以看到查出來的兩行數據完全相同:
當處理第一條數據時一切正常,而因為是嵌套映射則會將當前行數據緩存到 DefaultResultSetHandler#nestedResultObjects 中。當處理到第二條數據時,
在 DefaultResultSetHandler#applyNestedResultMappings 方法中從 nestedResultObjects 獲取到了緩存,從而不會將該行數據保存, 如下圖:
3. 解決方案
解決方案就是保證兩行數據不完全相同,比如這里可以通過增加 sys_user 的id 查詢保證數據的唯一性, 如下:
SELECTsr.*,su.id user_id,su.user_name user_user_name,su.PASSWORD user_passwordFROMsys_role srLEFT JOIN sys_user su ON sr.id = su.role_idwhere sr.id = 1
六、discriminator 標簽
我們以下面的情況為例:
<resultMap id="CollectionBaseResultMap" type="com.kingfish.entity.dto.SysUserDto" extends="BaseResultMap"><discriminator javaType="java.lang.Integer" column="id"><!-- value = '1' 的情況下是 resultType, Mybatis會為resultType自動生成一個 ResultMap, discriminatedMapId 是 com.kingfish.dao.SysUserDao.mapper_resultMap[CollectionBaseResultMap]_discriminator_case[1] --><case value="1" resultType="com.kingfish.entity.dto.SysUserDto"><result column="user_name" property="extend1"/></case><!-- value = '1' 的情況下是 resultMap, discriminatedMapId 即為 CollectionBaseResultMap 的id : com.kingfish.dao.SysUserDao.CollectionBaseResultMap--><case value="2" resultMap="CollectionBaseResultMap"><result column="nick_name" property="extend1"/></case></discriminator></resultMap>
這里需要注意 :
- discriminator 標簽中 case 中使用 resultType 和 resultMap 的 discriminatedMapId 并不相同, 返回類型是 resultType 時 則會自動生成一個 ResultMap,
- resultType情況下需要自己重新對名字進行轉換,因為沒有 ResultMap 的轉換,變量名無法對應。resultMap情況下會忽略 case 條件下的Result ,因為直接從緩存中獲取之前加載好的 CollectionBaseResultMap結構了。
七、其他
1. RowBounds
Mybatis可以通過傳參中的 RowBounds 可以完成邏輯分頁,但不推薦,因為所有的數據都是查詢到內存中再篩選。如下:
// 邏輯分頁查詢 :入參中有 RowBounds 參數List<SysMenuDto> selectByParam(RowBounds rowBounds);
2. ResultHandler
Mybatis可以通過傳參中的 ResultHandler 可以結果集處理,而不再通過 Mapper Method 方法再返回結果,如果不指定,則默認是通過 DefaultResultHandler 來處理。如下:
// 無返回值 && 入參中有 ResultHandler 實例void selectByParam(ResultHandler resultHandler);
官方對 ResultHandler 的說明【ResultHandler 參數允許自定義每行結果的處理過程。你可以將它添加到 List 中、創建 Map 和 Set,甚至丟棄每個返回值,只保留計算后的統計結果。你可以使用 ResultHandler 做很多事,這其實就是 MyBatis 構建 結果列表的內部實現辦法。】
需要注意的是
-
ResultHandler 要求方法必須無返回值,在 MapperMethod#execute 中會判斷進行該判斷:
-
DefaultResultSetHandler#handleResultSet 中判斷了如果指定了 ResultHandler 則使用指定的,否則使用 DefaultResultHandler:
3. @MapKey
官方描述 :供返回值為 Map 的方法使用的注解。它使用對象的某個屬性作為 key,將對象 List 轉化為 Map。屬性:value,指定作為 Map 的 key 值的對象屬性名。
即: 當一個查詢方法想要返回 Map 時,可以通過 @MapKey 來指定用來聚合的key 是什么字段,如下:
<select id="selectRoleForMap" resultMap="BaseResultMap">select *from sys_role</select
@MapKey("id")Map<Long, SysRoleDto> selectRoleForMap();
查詢結果會把 id 當做 Map 的key 字段來聚合,返回如下:
源碼處理邏輯在 :org.apache.ibatis.binding.MapperMethod#executeForMap
中,調用 DefaultSqlSession#selectMap 方法來處理,這里會交由 DefaultMapResultHandler 來處理結果, 將結果封裝成對應的 Map。
以上:內容部分參考
https://www.jianshu.com/p/05f643f27246
https://juejin.cn/post/6844904127818891278
如有侵擾,聯系刪除。 內容僅用于自我記錄學習使用。如有錯誤,歡迎指正