mybatis?plus框架@TableField注解不生效問題及解決方案
mybatis-plus手寫sql的時候@TableField注解不生效的問題剖析和解決方案
一、問題描述
最近遇到一個mybatis plus的問題,@TableField注解不生效,導(dǎo)致查出來的字段反序列化后為空
數(shù)據(jù)庫表結(jié)構(gòu):
CREATE TABLE `client_role` ( `id` int(11) NOT NULL AUTO_INCREMENT COMMENT '自增主鍵', `name` varchar(64) NOT NULL COMMENT '角色的唯一標(biāo)識', `desc` varchar(64) DEFAULT NULL COMMENT '角色描述', PRIMARY KEY (`id`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='角色表'
對應(yīng)的實體類
@Data @EqualsAndHashCode(callSuper = false) @Accessors(chain = true) @TableName("client_role") @ApiModel(value = "ClientRole對象", description = "角色表") public class ClientRole implements Serializable { private static final long serialVersionUID = 1L; /** * 自增主鍵 */ @ApiModelProperty(value = "自增主鍵") @TableId(value = "id", type = IdType.AUTO) private Long id; * 角色的唯一標(biāo)識 @NotEmpty @ApiModelProperty(value = "角色的唯一標(biāo)識") @TableField("name") private String name; * 角色描述 @ApiModelProperty(value = "角色描述") @TableField("`desc`") private String description; }
就是description字段為空的問題,查詢sql如下
<select id="selectOneByName" resultType="com.kdyzm.demo.springboot.entity.ClientRole"> select * from client_role where name = #{name}; </select>
然而,如果不手寫sql,使用mybatis plus自帶的LambdaQuery查詢,則description字段就有值了。
ClientRole admin = iClientRoleMapper.selectOne( new LambdaQueryWrapper<ClientRole>().eq(ClientRole::getName, "admin") );
真是活見鬼,兩種方法理論上結(jié)果應(yīng)該是一模一樣的,最終卻發(fā)現(xiàn)@TableField字段在手寫sql這種方式下失效了。
二、解決方案
定義ResultMap,在xml文件中定義如下
<resultMap type="com.kdyzm.demo.springboot.entity.ClientRole" id="ClientRoleResult"> <result property="id" column="id"/> <result property="name" column="name"/> <result property="description" column="desc"/> </resultMap> <select id="selectOneByName" resultMap="ClientRoleResult"> select * from client_role where name = #{name}; </select>
select標(biāo)簽中resultType改成resultMap,值為resultMap標(biāo)簽的id,這樣description字段就有值了。
問題很容易解決,但是有個問題需要問下為什么:為什么@TableField注解在手寫sql的時候就失效了呢?
三、關(guān)于@TableField注解失效原因的思考
當(dāng)數(shù)據(jù)庫字段和自定義的實體類中字段名不一致的時候,可以使用@TableField注解實現(xiàn)矯正,以上面的代碼為例,
ClientRole admin = iClientRoleMapper.selectOne( new LambdaQueryWrapper<ClientRole>().eq(ClientRole::getName, "admin") );
這段代碼被翻譯成sql,它被翻譯成這樣
好家伙,原來@TableField注解功能是通過加別名實現(xiàn)的。
那如果是手寫sql的話,它如何把別名加上去呢?答案就是沒辦法加上去,因為手寫sql太靈活了,不在mybatis plus功能框架內(nèi),那是屬于原生mybatis的功能范疇,不支持也就正常了。
四、Mapper接口LambdaQuery方法調(diào)用過程梳理
進(jìn)一步探討,@TableField注解是如何生成別名的呢,那就要研究下源碼了。
1、Mapper接口調(diào)用實際上使用的是動態(tài)代理技術(shù)
mybatis定義的都是一堆的接口,并沒有實現(xiàn)類,但是卻能正常調(diào)用,這很明顯使用了動態(tài)代理技術(shù),實際上注入spring的時候接口被包裝成了代理對象,這就為debug源碼提供了突破口。
可以看到,這個代理對象實際的類名為com.baomidou.mybatisplus.core.override.MybatisMapperProxy
,它實現(xiàn)了InvocationHandler接口,確定是JDK動態(tài)代理無疑了,那么所有的邏輯都會走com.baomidou.mybatisplus.core.override.MybatisMapperProxy#invoke
方法
2、mybatis plus對查詢的單獨處理
根據(jù)上面一步找到源碼的入口,一步一步走下去,接口調(diào)用到了com.baomidou.mybatisplus.core.override.MybatisMapperMethod#execute
方法
public Object execute(SqlSession sqlSession, Object[] args) { Object result; switch (command.getType()) { case INSERT: { Object param = method.convertArgsToSqlCommandParam(args); result = rowCountResult(sqlSession.insert(command.getName(), param)); break; } case UPDATE: { Object param = method.convertArgsToSqlCommandParam(args); result = rowCountResult(sqlSession.update(command.getName(), param)); break; } case DELETE: { Object param = method.convertArgsToSqlCommandParam(args); result = rowCountResult(sqlSession.delete(command.getName(), param)); break; } case SELECT: if (method.returnsVoid() && method.hasResultHandler()) { executeWithResultHandler(sqlSession, args); result = null; } else if (method.returnsMany()) { result = executeForMany(sqlSession, args); } else if (method.returnsMap()) { result = executeForMap(sqlSession, args); } else if (method.returnsCursor()) { result = executeForCursor(sqlSession, args); } else { // TODO 這里下面改了 if (IPage.class.isAssignableFrom(method.getReturnType())) { result = executeForIPage(sqlSession, args); // TODO 這里上面改了 } else { Object param = method.convertArgsToSqlCommandParam(args); result = sqlSession.selectOne(command.getName(), param); if (method.returnsOptional() && (result == null || !method.getReturnType().equals(result.getClass()))) { result = Optional.ofNullable(result); } } } break; case FLUSH: result = sqlSession.flushStatements(); break; default: throw new BindingException("Unknown execution method for: " + command.getName()); } if (result == null && method.getReturnType().isPrimitive() && !method.returnsVoid()) { throw new BindingException("Mapper method '" + command.getName() + " attempted to return null from a method with a primitive return type (" + method.getReturnType() + ")."); } return result; }
這段代碼特點在于它對于非查詢類型的請求(比如插入、更新和刪除),都直接委托給了sqlSeesion的相應(yīng)的方法調(diào)用,而對于查詢請求,則邏輯比較復(fù)雜,畢竟sql最復(fù)雜的地方就是查詢了;還有另外一個特點,針對不同的返回結(jié)果類型,也走不同的邏輯;由于我這里查詢返回的是一個實體對象,所以最終走到了如下斷點
從代碼上來看,也只是委托給了SqlSessionTemplate對象處理了,然而SqlSessionTemplate的全包名是org.mybatis.spring.SqlSessionTemplate
,它是mybatis集成spring的官方功能,和mybatis plus沒關(guān)系,就這如何能讓@TableField注解發(fā)揮作用呢?
3、findOne實際上還是要查詢List
繼續(xù)debug幾次,到了一個有趣的方法org.apache.ibatis.session.defaults.DefaultSqlSession#selectOne(java.lang.String, java.lang.Object)
原來單獨查詢一個對象,還是要查詢List,然后取出第一個對象返回;如果查詢出多個對象,則直接拋出TooManyResultsException
,建表的時候不做唯一索引查出來多個對象的時候拋出的異常就是在這里做的。
有意思的是,方法執(zhí)行到這里,傳參只有兩個,一個是方法名,另外一個是查詢參數(shù)
總之還是要繼續(xù)查看selectList的邏輯,才能搞清楚邏輯
4、mybatis接口上下文信息MappedStatement
上一步說到selectList方法調(diào)用只傳遞了兩個參數(shù),一個是方法名,一個是方法參數(shù),只是這兩個參數(shù)是無法滿足查詢的請求的,畢竟最重要的sql語句都沒傳,debug下去,到了一處比較重要的地方,就解開了我的疑問:org.apache.ibatis.session.defaults.DefaultSqlSession#selectList(java.lang.String, java.lang.Object, org.apache.ibatis.session.RowBounds)
在這個方法里,根據(jù)statement也就是方法名獲取到了MappedStatement對象,這個對象里存儲著這個關(guān)于本次查詢需要的上下文信息,繼續(xù)debug,來到一個方法com.baomidou.mybatisplus.core.executor.MybatisCachingExecutor#query(org.apache.ibatis.mapping.MappedStatement, java.lang.Object, org.apache.ibatis.session.RowBounds, org.apache.ibatis.session.ResultHandler)
它調(diào)用了MappedStatement對象的getBoundSql方法,便得到了帶有別名的sql字符串,也就是說,這個getBoundSql方法形成了這段sql字符串,debug進(jìn)去看看
5、mybatis plus別名自動設(shè)置的邏輯
debug ms.getBoundSql方法,最終到了方法:org.apache.ibatis.scripting.xmltags.MixedSqlNode#apply
,該方法入?yún)⑹?code>org.apache.ibatis.scripting.xmltags.DynamicContext類型,其內(nèi)部維護(hù)了一個java.util.StringJoiner
對象,專門用于拼接sql
contents對象是個List類表,其有八個元素,經(jīng)過八個元素的apply方法調(diào)用之后,DynamicContext的sqlBuilder對象就有了值了
原來別名是在這里設(shè)置的;這里先暫且不談,查詢流程還沒結(jié)束,先看整個的流程。
6、mybatis plus的sql日志打印
我們看到的sql日志是如何打印出來的?上一步已經(jīng)獲取到了sql,接下來繼續(xù)debug,就會看到sql打印的代碼:org.apache.ibatis.logging.jdbc.ConnectionLogger#invoke
7、最終查詢的執(zhí)行
我們知道,無論是mybatis還是其它框架,最終執(zhí)行查詢都要遵循java api規(guī)范,上一步已經(jīng)獲取到了PreparedStatement,最終在這個方法執(zhí)行了查詢
org.apache.ibatis.executor.statement.PreparedStatementHandler#query
8、結(jié)果集處理
查詢完之后要封裝結(jié)果集,封裝邏輯的起始方法:org.apache.ibatis.executor.resultset.DefaultResultSetHandler#handleResultSets
可以看到,這段邏輯就是在從Satement對象中循環(huán)取數(shù)據(jù),然后調(diào)用org.apache.ibatis.executor.resultset.DefaultResultSetHandler#handleResultSet方法處理每一條數(shù)據(jù)
9、每一條數(shù)據(jù)的單獨處理
繼續(xù)debug,可以看到對每一條結(jié)果數(shù)據(jù)的單獨處理的邏輯:org.apache.ibatis.executor.resultset.DefaultResultSetHandler#getRowValue(org.apache.ibatis.executor.resultset.ResultSetWrapper, org.apache.ibatis.mapping.ResultMap, java.lang.String)
這里首先使用自動字段名映射的方式填充返回值,然后使用resultMap繼續(xù)填充返回值,最后返回rowValue作為最終反序列化完成的值。
至此,整個查詢過程基本上就結(jié)束了。
五、@TableField注解生效原理
1、別名sql在mapper方法執(zhí)行前就已經(jīng)確定
上一步在梳理Mapper接口調(diào)用過程的時候在第5點說過,DynamicContext內(nèi)部維護(hù)了一個StringJoiner對象用于拼接sql,在經(jīng)過MixedSqlNode內(nèi)部的8個SqlNode處理之后,StringJoiner就有了完整的sql語句。我們知道@TableField生效的原理是設(shè)置別名,那么別名是這時候設(shè)置上去的嗎?
SqlNode有很多實現(xiàn)類,目測mybatis通過實現(xiàn)SqlNode接口實現(xiàn)對XML語法的支持。里面最簡單的SqlNode就是StaticTextSqlNode了
可以看到這個類內(nèi)部維護(hù)了一個text字符串,然后將這個text字符串掛到DynamicContext的StringJoiner,就是這么簡單的邏輯,然而別名sql就是在這里設(shè)置上去的:
答案已經(jīng)一目了然了,代碼在執(zhí)行到這里的時候,這個StaticTextSqlNode里面的text就已經(jīng)準(zhǔn)備好了sql了,等到它執(zhí)行apply方法的時候直接就給掛到了DynamicConetxt的StringJoiner,這說明了別名sql的設(shè)置在Mapper方法執(zhí)行之前就已經(jīng)確定了,而非是代碼執(zhí)行過程中動態(tài)的解析。
2、@TableField注解的外層解析
@TableFied注解何時被解析?可以推測肯定是mybatis plus starter搞的鬼,但是入口方法調(diào)用鏈很長,找到解析點會比較困難,最直接的方法就是在借助intelij工具,右鍵注解,findUseage,自然就找到了這個解析方法:com.baomidou.mybatisplus.core.metadata.TableInfoHelper#initTableFields
。在該方法上打上斷點,debug模式啟動服務(wù),就找到了調(diào)用鏈
可以看到,一切的起點就在com.baomidou.mybatisplus.autoconfigure.MybatisPlusAutoConfiguration
配置類,在方法com.baomidou.mybatisplus.autoconfigure.MybatisPlusAutoConfiguration#sqlSessionFactory
中創(chuàng)建SqlSessionFactory時開啟整個的解析流程,整個流程非常復(fù)雜,最終會調(diào)用到com.baomidou.mybatisplus.core.injector.AbstractSqlInjector#inspectInject
方法,在執(zhí)行完成com.baomidou.mybatisplus.core.metadata.TableInfoHelper#initTableInfo
方法之后,TableInfo對象中的fiedList就已經(jīng)存儲了數(shù)據(jù)庫字段和實體字段的映射關(guān)系:
initTableInfo方法內(nèi)部解析了@TableField注解,并且生成了數(shù)據(jù)庫字段和實體字段的映射關(guān)系,并最終保存到了TableInfo對象。
然而,這個實體對象無法直接使用,因為在前面Mapper接口調(diào)用梳理的過程中就知道了,在拼接sql的時候別名已經(jīng)以sql的形式存儲在了StaticTextSqlNode,還要繼續(xù)debug尋找轉(zhuǎn)換點
3、MappedStatement對象創(chuàng)建和保存
緊接著要執(zhí)行的代碼在循環(huán)注入自定義方法這塊,上一步解析好的TableInfo會被應(yīng)用到以下十七種內(nèi)置方法,這和我們常用的com.baomidou.mybatisplus.core.mapper.BaseMapper
接口中的方法數(shù)量是相同的,當(dāng)然也就不包括手寫sql的那個自定義方法。
在循環(huán)體上打上斷點,看看這個inject方法做了什么事情,由于我們只關(guān)心com.baomidou.mybatisplus.core.injector.methods.SelectOne
,所以直接進(jìn)入SelectOne的inject方法打上斷點
好家伙,這個sqlSource可太眼熟了,基本上可以確定這個和上面分析的5、mybatis plus別名自動設(shè)置的邏輯
中的DynamicSqlSource是同一個對象,如果將其放到MappedStatement對象內(nèi),那就和Mapper接口方法執(zhí)行的流程對的上了,從接下來執(zhí)行的方法addSelectMappedStatementForTable名字上來看,做的也正是這個事情,繼續(xù)debug,最終到了方法org.apache.ibatis.builder.MapperBuilderAssistant#addMappedStatement
該方法創(chuàng)建了MappedStatement對象,并且存儲到了全局Configuration對象。這樣,在執(zhí)行Mapper接口方法的時候,根據(jù)上面梳理的執(zhí)行流程中的4、mybatis接口上下文信息MappedStatement
,就可以在configuration對象中取出MappedStatement對象用于查詢了,這樣,就整個串通了@TableFied注解的作用過程。
4、一些疑問
上面梳理了LambdaQuery接口執(zhí)行的過程以及確定了@TableField注解在這個過程中是通過給字段起別名的方式實現(xiàn)了數(shù)據(jù)庫字段和實體字段的映射。其實還有幾處疑問需要解決
1、為啥手寫sql@TableField注解就失效了呢?雖然在三、關(guān)于@TableField注解失效原因的思考
中大體上明白了失效的合理性,但是從技術(shù)層面上來講只是搞明白了內(nèi)置方法對@TableFied注解的支持,還沒搞明白手寫sql為啥不支持@TableFied注解。再具體點,手寫sql肯定是沒有別名的,那它的DynamicSqlSource和內(nèi)置方法的DynamicSqlSource有何不同?手寫sql需要定義ResultMap,ResultMap在何時生效的?退一步說,手寫sql和內(nèi)置方法的查詢是否走的同一個查詢流程呢?
2、使用LambdaQuery的內(nèi)置方法通過下面的代碼生成MappedStatement對象并且保存到Configuration全局配置中,手寫的sql并不在這個列表中,手寫sql的接口方法何時創(chuàng)建的MappedStatement對象的呢?
六、Mapper接口手寫sql方法調(diào)用過程梳理
整個流程基本上和四、Mapper接口LambdaQuery方法調(diào)用過程梳理
一樣,這里只是說下不同之處
1、生成sql的方式不同
在LambdaQuery中,生成sql的方式是使用DynamicSqlSource
其內(nèi)部維護(hù)了一個rootSqlNode用于解析sql語句,其中查詢列包含別名被放到了一個StaticTextSqlNode中;
但是在手寫sql的時候,不再是DynamicSqlSource,而是RawSqlSource:
內(nèi)部不再維護(hù)MixedSqlNode,而是直接使使用一個sql字符串,該字符串正是xml文件中手寫的sql:
<select id="selectOneByName" resultMap="ClientRoleResult"> select * from client_role where name = #{name}; </select>
很明顯,這里確實是原生的sql,沒有任何的mybatis標(biāo)簽混雜在里面。
假如我稍微改一下這段sql又如何?改成如下形式
<select id="selectOneByName" resultMap="ClientRoleResult"> select * from client_role <where> name = #{name}; </where> </select>
兩段代碼邏輯上是完全一樣的,再次運行debug到此處
可以看到,sqlSource已經(jīng)變成了DynamicSqlSource,只是它相對于LambdaQuery的查詢方式,少了很多個SqlNode節(jié)點。雖然變成了DynamicSqlSource,但是可以看到還是沒有設(shè)置別名,StaticTextSqlNode中存儲了xml文件中寫的原始的sql字符串。
這樣可以得出結(jié)論:如果xml文件中寫的sql沒有使用任何mybatis的標(biāo)簽,則會使用RawSqlSource,如果使用了例如<where></where>
等標(biāo)簽,則會使用DynamicSqlSource;同樣使用的都是DynamicSqlSource的情況下,手寫Sql的DynamicSqlSource查詢列不會自動增加別名,查詢列取決于手寫sql的代碼。
需要注意的是執(zhí)行這段代碼的是org.apache.ibatis.mapping.MappedStatement
對象,它是在服務(wù)啟動的時候創(chuàng)建并保存到全局MybatisConfiguration中的,也就是說,在服務(wù)啟動的時候就已經(jīng)決定了在這里查詢的時候使用的是DynamicSqlSource還是RawSqlSource。
2、結(jié)果集處理方式不同
之前說過,即使是查詢一個元素,底層還是會查詢List,然后對每個元素單獨反序列化封裝成實體類對象,這個操作在org.apache.ibatis.executor.resultset.DefaultResultSetHandler#getRowValue(org.apache.ibatis.executor.resultset.ResultSetWrapper, org.apache.ibatis.mapping.ResultMap, java.lang.String)
方法中。
需要注意的是402行的applyAutomaticMappings方法執(zhí)行以及404行的applyPropertyMappings方法執(zhí)行
當(dāng)使用LambdaQuery查詢的時候,402行代碼返回的foundValues值為true,方法執(zhí)行完成,rowValue就有值了,見下圖:
404行的applyPropertyMappings方法執(zhí)行則會直接跳過執(zhí)行,因為不滿足執(zhí)行條件;
而當(dāng)手寫sql方法調(diào)用時,402行的applyAutomaticMappings方法執(zhí)行會返回false,執(zhí)行完成之后rowValue字段屬性并沒有填充,見下圖:
而404行的applyPropertyMappings方法滿足了執(zhí)行條件,執(zhí)行完成之后foundValues的值變成了true,而rawValue也有值了。
為啥呢?
applyAutomaticMappings方法和applyPropertyMappings方法兩個方法從方法名字上來看似乎是對立的兩個方法如果未指定PropertieyMapping,則走applyAutomacitMapping,如果指定了則走applyPropertyMapping,但是會不會同時存在兩個方法都走一遍呢?那是肯定的,因為applyPropertyMapping并沒有放在else塊中,它是強制執(zhí)行的,為了驗證這個問題,修改下Xml文件中定義的ResultMap,原來ResultMap長這樣子
<resultMap type="com.kdyzm.demo.springboot.entity.ClientRole" id="ClientRoleResult"> <result property="id" column="id"/> <result property="name" column="name"/> <result property="description" column="desc"/> </resultMap>
現(xiàn)在我改成這個樣子
<resultMap type="com.kdyzm.demo.springboot.entity.ClientRole" id="ClientRoleResult"> <result property="description" column="desc"/> </resultMap>
刪掉表字段和實體字段同名的映射關(guān)系,只留下不同的映射關(guān)系,再次執(zhí)行手寫sql的接口查詢。
執(zhí)行完成applyAutomaticMappings方法之后,未在ResultMap中指定映射關(guān)系的id和name兩個屬性填充上了值,如下圖:
執(zhí)行完成applyPropertyMappings方法之后,在ResultMap中定了映射關(guān)系的description字段填充上了值,如下圖:
說明了一個問題:只要在ResultMap中沒定義映射關(guān)系,就會被applyAutomaticMappings方法處理屬性填充;如果在ResultMap中定義了映射關(guān)系,則會被applyPropertyMappings方法處理屬性填充;另外,說明了ResultMap不需要全部都寫上關(guān)系映射,只需要寫數(shù)據(jù)庫字段名和實體類字段不一致的映射即可。
那么如何區(qū)分出來哪些字段該走applyAutomaticMappings方法屬性填充,哪些字段該走applyPropertyMappings屬性填充呢?
答案就在傳過來的resultMap對象中,它有個屬性叫ResultMapping,存儲著解析XML文件中ResultMap的映射,如下圖所示:
凡是在resultMapping中的屬性,都走applyPropertyMappings方法,否則走applyAutomaticMappings方法。
3、手寫sql接口方法@TableFied注解失效的原因
一開始未在xml文件中定義ResultMapping,且使用的是手寫sql。根據(jù)上面的源碼分析,在未定義ResultMap的情況下,所有的屬性填充都會走org.apache.ibatis.executor.resultset.DefaultResultSetHandler#applyAutomaticMappings
方法,其邏輯也比較清晰
首先查找出所有未在xml文件中定義的ResultMap映射表字段集合對這些表字段進(jìn)行處理,比如如果開啟了mapUnderscoreToCamelCase,則會將表字段從下?lián)Q線變成駝峰命名嘗試從實體類中尋找轉(zhuǎn)換好的字段,如果找到了,則全部放到List<UnMappedColumnAutoMapping> autoMapping
從mapping尋找適合的typeHandler解析屬性值,比如Long類型的值會調(diào)用LongTypeHandler進(jìn)行屬性值解析屬性值填充到rawValue
套用上述流程,看看description字段為啥沒填充上去:
- 首先查找出所有未在xml文件中定義的ResultMap映射表字段集合,找到了id,name,desc三個表字段
- 對這些表字段進(jìn)行處理,比如如果開啟了mapUnderscoreToCamelCase,則會將表字段從下?lián)Q線變成駝峰命名,三個字段都無變化
- 嘗試從實體類尋找轉(zhuǎn)換好的字段,如果找到了,則全部放到
List<UnMappedColumnAutoMapping> autoMapping
,實體類有三個字段id,name,description,id,name都找到了,由于desc和description長得不一樣,所以就沒填充到List<UnMappedColumnAutoMapping> autoMapping
,最終上圖中只有id和name兩個屬性值被add到了autoMapping。從mapping尋找適合的typeHandler解析 - 屬性值,這里只解析了id和name兩個字段的屬性值屬性值填充到rawValue,這里只填充了id和name兩個字段的屬性值
總結(jié)下,desc字段因為沒有在ResultMap中定義,所以不會被applyPropertyMappings方法處理;本來應(yīng)該被applyAutomaticMappings處理的,又因為和description實體類字段名長得不一樣,就被applyAutomaticMappings方法忽略了,成了一個兩不管的狀態(tài),所以最終只能是默認(rèn)值填充,那就是null了。
那么@TableFied字段真的一點用就沒了嗎,上述流程中代碼中怎么知道數(shù)據(jù)庫表字段的呢?
表字段都被封裝到了ResultSetWrapper對象中,如下圖所示
這些表字段是從執(zhí)行結(jié)果ResultSet中的元數(shù)據(jù)獲取到的,最終通過構(gòu)造方法填充屬性值,如下圖所示
所以,當(dāng)手寫sql的時候,@TableField注解就真的完全沒用了。
下面說下手寫sqlmapper方法創(chuàng)建對應(yīng)MappedStatement對象的過程。
4、手寫SQL的MappedStatement對象的創(chuàng)建
同樣的,手寫sql的MappedStatement對象的創(chuàng)建也是在SqlSessionFactoryBean對象創(chuàng)建的過程中創(chuàng)建的。但是手寫SQL的MappedStatement對象創(chuàng)建的時間遠(yuǎn)比mybatis plus內(nèi)置方法的創(chuàng)建早的多。
創(chuàng)建SqlSessionFactoryBean的入口方法:com.baomidou.mybatisplus.extension.spring.MybatisSqlSessionFactoryBean#buildSqlSessionFactory
這段代碼會解析所有的xml文件并且最終在org.apache.ibatis.builder.MapperBuilderAssistant#addMappedStatement
方法中創(chuàng)建手寫sql的MapperStatement并保存到Configuration上下文中。
七、手寫SQL如何讓@TableFiled生效
如果,我就是想手寫SQL,還不想寫ResultMap而且還想@TableField注解生效,又該怎么做呢?
先說下結(jié)論:理論上可行,實踐很困難。下面逐一分析各種方法的可行性。
1、方法一:新增ResultMapping
通過上面的源碼分析,知道了mybatis針對每個Mapper接口都創(chuàng)建了一個MappedStatement對象,該對象實際上存儲了該接口的上下文信息,無論是執(zhí)行的sql還是結(jié)果類型、字段Mapping等都在里面(不包含ResultSet返回的行動態(tài)AutoMapping),在反序列化之前修改該對象,根據(jù)@TableFied注解新增數(shù)據(jù)庫字段和實體類字段的映射關(guān)系,就應(yīng)該能影響反序列化結(jié)果。
然而,我發(fā)現(xiàn)所有相關(guān)的屬性都被修飾成了不可修改的集合,這里有個最關(guān)鍵的resultMappings集合,也被修飾成了不可修改的集合,看起來官方并不想我們動他們的數(shù)據(jù),畢竟萬一出了問題,就很難排查是誰導(dǎo)致的了。
所以說,這種方式行不通。
2、方法二:使用插件填充未被設(shè)置值的屬性
如果沒設(shè)置ResultMap,會使用自動映射的方式填充實體類對象,desc和descriptin字段的映射則會失敗,最終到實體類對象里descriptin字段就為空。若是基于此結(jié)果,再做處理,將為空的值嘗試使用@TableFiled注解做映射再次填充,理論上也是可行的,所以我使用mybatis插件的方式重新處理了結(jié)果:
import com.baomidou.mybatisplus.annotation.TableField; import lombok.AllArgsConstructor; import lombok.Data; import lombok.extern.slf4j.Slf4j; import org.apache.ibatis.executor.resultset.DefaultResultSetHandler; import org.apache.ibatis.executor.resultset.ResultSetHandler; import org.apache.ibatis.executor.resultset.ResultSetWrapper; import org.apache.ibatis.mapping.MappedStatement; import org.apache.ibatis.mapping.ResultMap; import org.apache.ibatis.plugin.*; import org.apache.ibatis.session.Configuration; import org.apache.ibatis.type.JdbcType; import org.apache.ibatis.type.TypeHandler; import org.springframework.util.CollectionUtils; import java.lang.reflect.Field; import java.lang.reflect.Method; import java.lang.reflect.Modifier; import java.sql.ResultSet; import java.sql.SQLException; import java.sql.Statement; import java.util.*; /** * @author kdyzm * @date 2022/1/24 */ @Slf4j @Intercepts({ @Signature( type = ResultSetHandler.class, method = "handleResultSets", args = {Statement.class} ) }) public class ResultSetHandlerPlugin implements Interceptor { private final Map<String, List<UnMappedColumnMapping>> unMappedColumnMappingCache = new HashMap<>(); private ResultSetWrapper getFirstResultSet(Statement stmt, Configuration configuration) throws SQLException { ResultSet rs = stmt.getResultSet(); while (rs == null) { // move forward to get the first resultset in case the driver // doesn't return the resultset as the first result (HSQLDB 2.1) if (stmt.getMoreResults()) { rs = stmt.getResultSet(); } else { if (stmt.getUpdateCount() == -1) { // no more results. Must be no resultset break; } } } return rs != null ? new ResultSetWrapper(rs, configuration) : null; } @Override public Object intercept(Invocation invocation) throws Throwable { //通過StatementHandler獲取執(zhí)行的sql DefaultResultSetHandler statementHandler = (DefaultResultSetHandler) invocation.getTarget(); MappedStatement mappedStatement = getMappedStatement(statementHandler); Configuration configuration = mappedStatement.getConfiguration(); Object[] args = invocation.getArgs(); Method method = invocation.getMethod(); Statement statement = (Statement) invocation.getArgs()[0]; ResultSetWrapper firstResultSet = getFirstResultSet(statement, configuration); List result = (List) invocation.proceed(); //獲得結(jié)果集 ResultMap resultMap = mappedStatement.getResultMaps().get(0); List<UnMappedColumnMapping> unMappedColumnMappings = getUnMappedColumnMapping(firstResultSet, resultMap); //TODO return result; private List<UnMappedColumnMapping> getUnMappedColumnMapping(ResultSetWrapper firstResultSet, ResultMap resultMap) { Class clazz = resultMap.getType(); List<UnMappedColumnMapping> unMappedColumnMappings = this.unMappedColumnMappingCache.get(clazz.getName()); if (!CollectionUtils.isEmpty(unMappedColumnMappings)) { return unMappedColumnMappings; unMappedColumnMappings = new ArrayList<>(); Set<String> mappedProperties = resultMap.getMappedProperties(); Field[] fields = clazz.getDeclaredFields(); for (Field field : fields) { if (Modifier.isFinal(field.getModifiers()) || Modifier.isStatic(field.getModifiers()) || Modifier.isVolatile(field.getModifiers()) ) { continue; String fieldName = field.getName(); boolean contains = mappedProperties.contains(fieldName); if (contains) { TableField annotation = field.getAnnotation(TableField.class); if (Objects.isNull(annotation)) { String columnName = annotation.value(); TypeHandler<?> typeHandler = firstResultSet.getTypeHandler(field.getType(), columnName); if (Objects.isNull(typeHandler)) { log.error("不支持的字段反序列化:{}", columnName); log.info("字段={}使用的反序列化工具為:{}", columnName, typeHandler); UnMappedColumnMapping unMappedColumnMapping = new UnMappedColumnMapping( columnName, field.getName(), typeHandler ); unMappedColumnMappings.add(unMappedColumnMapping); this.unMappedColumnMappingCache.put(clazz.getName(), unMappedColumnMappings); return unMappedColumnMappings; private MappedStatement getMappedStatement(DefaultResultSetHandler statementHandler) throws NoSuchFieldException, IllegalAccessException { Field field = statementHandler.getClass().getDeclaredField("mappedStatement"); field.setAccessible(true); MappedStatement mappedStatement = (MappedStatement) field.get(statementHandler); return mappedStatement; public Object plugin(Object target) { return Plugin.wrap(target, this); @Data @AllArgsConstructor static class UnMappedColumnMapping { private String columnName; private String propertyName; private TypeHandler<?> typeHandler; }
代碼寫到上述TODO的地方就寫不下去了。。。原因是Satement對象中的結(jié)果只能讀一次,在第一次List result = (List) invocation.proceed();
執(zhí)行過后,再次取結(jié)果就取不出來了。
而且coding的過程中發(fā)現(xiàn)其它的問題:MappedStatement對象作為DefaultResultSetHandler的成員變量并沒有暴露GET/SET方法,要想獲取到必須通過反射暴力獲?。?/p>
private MappedStatement getMappedStatement(DefaultResultSetHandler statementHandler) throws NoSuchFieldException, IllegalAccessException { Field field = statementHandler.getClass().getDeclaredField("mappedStatement"); field.setAccessible(true); MappedStatement mappedStatement = (MappedStatement) field.get(statementHandler); return mappedStatement; }
在我感覺其實很不爽,畢竟強扭的瓜不甜。。。
總而言之,這種方式也以失敗告終,那只能用最后一種終極方法了:自定義反序列化的過程。
3、方法三:自定義反序列化過程
這種方式確實可以實現(xiàn),但是實現(xiàn)起來會很困難,因為不想破壞mybatis和mybaits plus原有的功能,比如:autoMapping、下劃線轉(zhuǎn)駝峰、resultMap、各種返回類型處理。。。如果自己重新實現(xiàn),代價就太大了,這是得不償失的做法。如果不破壞這些功能,只是稍微做些修改的話是可以接受的。
4、方法四:增強反序列化過程
首先制定一個反序列化的規(guī)則:當(dāng)手寫sql的時候,自動mapping和resultmap優(yōu)先級最高,之后若是有未匹配的屬性,則使用@TableField注解嘗試解決,最終如果還是無法匹配,則直接pass掉不做處理。
這里處理的核心方法就是在mybatis反序列化處理完單個對象之后額外添加邏輯,核心方法就在:DefaultResultSetHandler#getRowValue(org.apache.ibatis.executor.resultset.ResultSetWrapper, org.apache.ibatis.mapping.ResultMap, java.lang.String)
方法中
問題就在于此處代碼無法應(yīng)用動態(tài)代理或者切面技術(shù),最終,我使用了javassit技術(shù)動態(tài)修改字節(jié)碼對象解決了該問題,javassit簡介可以參考文章:使用javassist運行時動態(tài)修改字節(jié)碼對象
項目源代碼地址:狂盜一枝梅 / mybatis-plus-fix
最終,實現(xiàn)效果上來看,確實解決了@TableFiled注解在手寫sql的情況下失效的問題,但是由于額外執(zhí)行了一段代碼,所以執(zhí)行效率會稍微低一些;而且由于使用了javassit,代碼的可讀性和可維護(hù)性較低,尤其是在debug代碼的時候會出現(xiàn)靈異現(xiàn)象。。。綜上,作為實驗性的問題解決,雖然能解決問題,但是不建議使用,哈哈
到此這篇關(guān)于mybatis plus框架的@TableField注解不生效問題總結(jié)的文章就介紹到這了,更多相關(guān)mybatis plus @TableField注解不生效內(nèi)容請搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關(guān)文章希望大家以后多多支持腳本之家!
- j2ee mybatis注解@Data,@TableName,@TableField使用方式
- 注解@TableName,@TableField,pgsql的模式對應(yīng)方式
- MyBatisPlus中@TableField注解的基本使用
- mybatis-plus常用注解@TableId和@TableField的用法
- Java如何獲取@TableField,@TableName注解的值
- MyBatisPlus使用@TableField注解處理默認(rèn)填充時間的問題
- Mybatis-plus使用注解 @TableField(exist = false)
- @TableField注解之深入理解與應(yīng)用方式
相關(guān)文章
Java Spring5學(xué)習(xí)之JdbcTemplate詳解
這篇文章主要介紹了Java Spring5學(xué)習(xí)之JdbcTemplate詳解,文中有非常詳細(xì)的代碼示例,對正在學(xué)習(xí)java的小伙伴們有非常好的幫助,需要的朋友可以參考下2021-05-05SpringBoot動態(tài)定時任務(wù)實現(xiàn)與應(yīng)用詳解
定時任務(wù)在許多應(yīng)用場景中是必不可少的,特別是在自動化任務(wù)執(zhí)行、定期數(shù)據(jù)處理等方面,定時任務(wù)能極大地提高系統(tǒng)的效率,然而,隨著業(yè)務(wù)需求的變化,定時任務(wù)的執(zhí)行頻率或時間點可能需要動態(tài)調(diào)整,所以本文給大家介紹了SpringBoot動態(tài)定時任務(wù)實現(xiàn)與應(yīng)用2024-08-08Mybatis報錯mapkey is required問題及解決
這篇文章主要介紹了Mybatis報錯mapkey is required問題及解決,具有很好的參考價值,希望對大家有所幫助,如有錯誤或未考慮完全的地方,望不吝賜教2024-06-06