欧美bbbwbbbw肥妇,免费乱码人妻系列日韩,一级黄片

mybatis?plus框架@TableField注解不生效問(wèn)題及解決方案

 更新時(shí)間:2022年03月04日 17:29:12   作者:狂盜一枝梅  
最近遇到一個(gè)mybatis plus的問(wèn)題,@TableField注解不生效,導(dǎo)致查出來(lái)的字段反序列化后為空,今天通過(guò)本文給大家介紹下mybatis?plus框架的@TableField注解不生效問(wèn)題總結(jié),需要的朋友可以參考下

mybatis-plus手寫(xiě)sql的時(shí)候@TableField注解不生效的問(wèn)題剖析和解決方案

一、問(wèn)題描述

最近遇到一個(gè)mybatis plus的問(wèn)題,@TableField注解不生效,導(dǎo)致查出來(lái)的字段反序列化后為空

數(shù)據(jù)庫(kù)表結(jié)構(gòu):

CREATE TABLE `client_role` (
  `id` int(11) NOT NULL AUTO_INCREMENT COMMENT '自增主鍵',
  `name` varchar(64) NOT NULL COMMENT '角色的唯一標(biāo)識(shí)',
  `desc` varchar(64) DEFAULT NULL COMMENT '角色描述',
  PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='角色表'

對(duì)應(yīng)的實(shí)體類(lèi)

@Data
@EqualsAndHashCode(callSuper = false)
@Accessors(chain = true)
@TableName("client_role")
@ApiModel(value = "ClientRole對(duì)象", 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)識(shí)
    @NotEmpty
    @ApiModelProperty(value = "角色的唯一標(biāo)識(shí)")
    @TableField("name")
    private String name;
     * 角色描述
    @ApiModelProperty(value = "角色描述")
    @TableField("`desc`")
    private String description;
}

就是description字段為空的問(wèn)題,查詢(xún)sql如下

 <select id="selectOneByName" resultType="com.kdyzm.demo.springboot.entity.ClientRole">
    select *
    from client_role
    where name = #{name};
  </select>

然而,如果不手寫(xiě)sql,使用mybatis plus自帶的LambdaQuery查詢(xún),則description字段就有值了。

ClientRole admin = iClientRoleMapper.selectOne(
    new LambdaQueryWrapper<ClientRole>().eq(ClientRole::getName, "admin")
);

真是活見(jiàn)鬼,兩種方法理論上結(jié)果應(yīng)該是一模一樣的,最終卻發(fā)現(xiàn)@TableField字段在手寫(xiě)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字段就有值了。

問(wèn)題很容易解決,但是有個(gè)問(wèn)題需要問(wèn)下為什么:為什么@TableField注解在手寫(xiě)sql的時(shí)候就失效了呢?

三、關(guān)于@TableField注解失效原因的思考

當(dāng)數(shù)據(jù)庫(kù)字段和自定義的實(shí)體類(lèi)中字段名不一致的時(shí)候,可以使用@TableField注解實(shí)現(xiàn)矯正,以上面的代碼為例,

ClientRole admin = iClientRoleMapper.selectOne(
    new LambdaQueryWrapper<ClientRole>().eq(ClientRole::getName, "admin")
);

這段代碼被翻譯成sql,它被翻譯成這樣

好家伙,原來(lái)@TableField注解功能是通過(guò)加別名實(shí)現(xiàn)的。

那如果是手寫(xiě)sql的話,它如何把別名加上去呢?答案就是沒(méi)辦法加上去,因?yàn)槭謱?xiě)sql太靈活了,不在mybatis plus功能框架內(nèi),那是屬于原生mybatis的功能范疇,不支持也就正常了。

四、Mapper接口LambdaQuery方法調(diào)用過(guò)程梳理

進(jìn)一步探討,@TableField注解是如何生成別名的呢,那就要研究下源碼了。

1、Mapper接口調(diào)用實(shí)際上使用的是動(dòng)態(tài)代理技術(shù)

mybatis定義的都是一堆的接口,并沒(méi)有實(shí)現(xiàn)類(lèi),但是卻能正常調(diào)用,這很明顯使用了動(dòng)態(tài)代理技術(shù),實(shí)際上注入spring的時(shí)候接口被包裝成了代理對(duì)象,這就為debug源碼提供了突破口。

可以看到,這個(gè)代理對(duì)象實(shí)際的類(lèi)名為com.baomidou.mybatisplus.core.override.MybatisMapperProxy,它實(shí)現(xiàn)了InvocationHandler接口,確定是JDK動(dòng)態(tài)代理無(wú)疑了,那么所有的邏輯都會(huì)走com.baomidou.mybatisplus.core.override.MybatisMapperProxy#invoke方法

2、mybatis plus對(duì)查詢(xún)的單獨(dú)處理

根據(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;
    }

這段代碼特點(diǎn)在于它對(duì)于非查詢(xún)類(lèi)型的請(qǐng)求(比如插入、更新和刪除),都直接委托給了sqlSeesion的相應(yīng)的方法調(diào)用,而對(duì)于查詢(xún)請(qǐng)求,則邏輯比較復(fù)雜,畢竟sql最復(fù)雜的地方就是查詢(xún)了;還有另外一個(gè)特點(diǎn),針對(duì)不同的返回結(jié)果類(lèi)型,也走不同的邏輯;由于我這里查詢(xún)返回的是一個(gè)實(shí)體對(duì)象,所以最終走到了如下斷點(diǎn)

從代碼上來(lái)看,也只是委托給了SqlSessionTemplate對(duì)象處理了,然而SqlSessionTemplate的全包名是org.mybatis.spring.SqlSessionTemplate,它是mybatis集成spring的官方功能,和mybatis plus沒(méi)關(guān)系,就這如何能讓@TableField注解發(fā)揮作用呢?

3、findOne實(shí)際上還是要查詢(xún)List

繼續(xù)debug幾次,到了一個(gè)有趣的方法org.apache.ibatis.session.defaults.DefaultSqlSession#selectOne(java.lang.String, java.lang.Object)

原來(lái)單獨(dú)查詢(xún)一個(gè)對(duì)象,還是要查詢(xún)List,然后取出第一個(gè)對(duì)象返回;如果查詢(xún)出多個(gè)對(duì)象,則直接拋出TooManyResultsException,建表的時(shí)候不做唯一索引查出來(lái)多個(gè)對(duì)象的時(shí)候拋出的異常就是在這里做的。

有意思的是,方法執(zhí)行到這里,傳參只有兩個(gè),一個(gè)是方法名,另外一個(gè)是查詢(xún)參數(shù)

總之還是要繼續(xù)查看selectList的邏輯,才能搞清楚邏輯

4、mybatis接口上下文信息MappedStatement

上一步說(shuō)到selectList方法調(diào)用只傳遞了兩個(gè)參數(shù),一個(gè)是方法名,一個(gè)是方法參數(shù),只是這兩個(gè)參數(shù)是無(wú)法滿足查詢(xún)的請(qǐng)求的,畢竟最重要的sql語(yǔ)句都沒(méi)傳,debug下去,到了一處比較重要的地方,就解開(kāi)了我的疑問(wèn):org.apache.ibatis.session.defaults.DefaultSqlSession#selectList(java.lang.String, java.lang.Object, org.apache.ibatis.session.RowBounds)

在這個(gè)方法里,根據(jù)statement也就是方法名獲取到了MappedStatement對(duì)象,這個(gè)對(duì)象里存儲(chǔ)著這個(gè)關(guān)于本次查詢(xún)需要的上下文信息,繼續(xù)debug,來(lái)到一個(gè)方法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對(duì)象的getBoundSql方法,便得到了帶有別名的sql字符串,也就是說(shuō),這個(gè)getBoundSql方法形成了這段sql字符串,debug進(jìn)去看看

5、mybatis plus別名自動(dòng)設(shè)置的邏輯

debug ms.getBoundSql方法,最終到了方法:org.apache.ibatis.scripting.xmltags.MixedSqlNode#apply,該方法入?yún)⑹?code>org.apache.ibatis.scripting.xmltags.DynamicContext類(lèi)型,其內(nèi)部維護(hù)了一個(gè)java.util.StringJoiner對(duì)象,專(zhuān)門(mén)用于拼接sql

contents對(duì)象是個(gè)List類(lèi)表,其有八個(gè)元素,經(jīng)過(guò)八個(gè)元素的apply方法調(diào)用之后,DynamicContext的sqlBuilder對(duì)象就有了值了

原來(lái)別名是在這里設(shè)置的;這里先暫且不談,查詢(xún)流程還沒(méi)結(jié)束,先看整個(gè)的流程。

6、mybatis plus的sql日志打印

我們看到的sql日志是如何打印出來(lái)的?上一步已經(jīng)獲取到了sql,接下來(lái)繼續(xù)debug,就會(huì)看到sql打印的代碼:org.apache.ibatis.logging.jdbc.ConnectionLogger#invoke

7、最終查詢(xún)的執(zhí)行

我們知道,無(wú)論是mybatis還是其它框架,最終執(zhí)行查詢(xún)都要遵循java api規(guī)范,上一步已經(jīng)獲取到了PreparedStatement,最終在這個(gè)方法執(zhí)行了查詢(xún)

org.apache.ibatis.executor.statement.PreparedStatementHandler#query

8、結(jié)果集處理

查詢(xún)完之后要封裝結(jié)果集,封裝邏輯的起始方法:org.apache.ibatis.executor.resultset.DefaultResultSetHandler#handleResultSets

可以看到,這段邏輯就是在從Satement對(duì)象中循環(huán)取數(shù)據(jù),然后調(diào)用org.apache.ibatis.executor.resultset.DefaultResultSetHandler#handleResultSet方法處理每一條數(shù)據(jù)

9、每一條數(shù)據(jù)的單獨(dú)處理

繼續(xù)debug,可以看到對(duì)每一條結(jié)果數(shù)據(jù)的單獨(dú)處理的邏輯:org.apache.ibatis.executor.resultset.DefaultResultSetHandler#getRowValue(org.apache.ibatis.executor.resultset.ResultSetWrapper, org.apache.ibatis.mapping.ResultMap, java.lang.String)

這里首先使用自動(dòng)字段名映射的方式填充返回值,然后使用resultMap繼續(xù)填充返回值,最后返回rowValue作為最終反序列化完成的值。

至此,整個(gè)查詢(xún)過(guò)程基本上就結(jié)束了。

五、@TableField注解生效原理

1、別名sql在mapper方法執(zhí)行前就已經(jīng)確定

上一步在梳理Mapper接口調(diào)用過(guò)程的時(shí)候在第5點(diǎn)說(shuō)過(guò),DynamicContext內(nèi)部維護(hù)了一個(gè)StringJoiner對(duì)象用于拼接sql,在經(jīng)過(guò)MixedSqlNode內(nèi)部的8個(gè)SqlNode處理之后,StringJoiner就有了完整的sql語(yǔ)句。我們知道@TableField生效的原理是設(shè)置別名,那么別名是這時(shí)候設(shè)置上去的嗎?

SqlNode有很多實(shí)現(xiàn)類(lèi),目測(cè)mybatis通過(guò)實(shí)現(xiàn)SqlNode接口實(shí)現(xiàn)對(duì)XML語(yǔ)法的支持。里面最簡(jiǎn)單的SqlNode就是StaticTextSqlNode了

可以看到這個(gè)類(lèi)內(nèi)部維護(hù)了一個(gè)text字符串,然后將這個(gè)text字符串掛到DynamicContext的StringJoiner,就是這么簡(jiǎn)單的邏輯,然而別名sql就是在這里設(shè)置上去的:

答案已經(jīng)一目了然了,代碼在執(zhí)行到這里的時(shí)候,這個(gè)StaticTextSqlNode里面的text就已經(jīng)準(zhǔn)備好了sql了,等到它執(zhí)行apply方法的時(shí)候直接就給掛到了DynamicConetxt的StringJoiner,這說(shuō)明了別名sql的設(shè)置在Mapper方法執(zhí)行之前就已經(jīng)確定了,而非是代碼執(zhí)行過(guò)程中動(dòng)態(tài)的解析。

2、@TableField注解的外層解析

@TableFied注解何時(shí)被解析?可以推測(cè)肯定是mybatis plus starter搞的鬼,但是入口方法調(diào)用鏈很長(zhǎng),找到解析點(diǎn)會(huì)比較困難,最直接的方法就是在借助intelij工具,右鍵注解,findUseage,自然就找到了這個(gè)解析方法:com.baomidou.mybatisplus.core.metadata.TableInfoHelper#initTableFields。在該方法上打上斷點(diǎn),debug模式啟動(dòng)服務(wù),就找到了調(diào)用鏈

可以看到,一切的起點(diǎn)就在com.baomidou.mybatisplus.autoconfigure.MybatisPlusAutoConfiguration配置類(lèi),在方法com.baomidou.mybatisplus.autoconfigure.MybatisPlusAutoConfiguration#sqlSessionFactory中創(chuàng)建SqlSessionFactory時(shí)開(kāi)啟整個(gè)的解析流程,整個(gè)流程非常復(fù)雜,最終會(huì)調(diào)用到com.baomidou.mybatisplus.core.injector.AbstractSqlInjector#inspectInject方法,在執(zhí)行完成com.baomidou.mybatisplus.core.metadata.TableInfoHelper#initTableInfo方法之后,TableInfo對(duì)象中的fiedList就已經(jīng)存儲(chǔ)了數(shù)據(jù)庫(kù)字段和實(shí)體字段的映射關(guān)系:

initTableInfo方法內(nèi)部解析了@TableField注解,并且生成了數(shù)據(jù)庫(kù)字段和實(shí)體字段的映射關(guān)系,并最終保存到了TableInfo對(duì)象。

然而,這個(gè)實(shí)體對(duì)象無(wú)法直接使用,因?yàn)樵谇懊鍹apper接口調(diào)用梳理的過(guò)程中就知道了,在拼接sql的時(shí)候別名已經(jīng)以sql的形式存儲(chǔ)在了StaticTextSqlNode,還要繼續(xù)debug尋找轉(zhuǎn)換點(diǎn)

3、MappedStatement對(duì)象創(chuàng)建和保存

緊接著要執(zhí)行的代碼在循環(huán)注入自定義方法這塊,上一步解析好的TableInfo會(huì)被應(yīng)用到以下十七種內(nèi)置方法,這和我們常用的com.baomidou.mybatisplus.core.mapper.BaseMapper接口中的方法數(shù)量是相同的,當(dāng)然也就不包括手寫(xiě)sql的那個(gè)自定義方法。

在循環(huán)體上打上斷點(diǎn),看看這個(gè)inject方法做了什么事情,由于我們只關(guān)心com.baomidou.mybatisplus.core.injector.methods.SelectOne,所以直接進(jìn)入SelectOne的inject方法打上斷點(diǎn)

好家伙,這個(gè)sqlSource可太眼熟了,基本上可以確定這個(gè)和上面分析的5、mybatis plus別名自動(dòng)設(shè)置的邏輯中的DynamicSqlSource是同一個(gè)對(duì)象,如果將其放到MappedStatement對(duì)象內(nèi),那就和Mapper接口方法執(zhí)行的流程對(duì)的上了,從接下來(lái)執(zhí)行的方法addSelectMappedStatementForTable名字上來(lái)看,做的也正是這個(gè)事情,繼續(xù)debug,最終到了方法org.apache.ibatis.builder.MapperBuilderAssistant#addMappedStatement

該方法創(chuàng)建了MappedStatement對(duì)象,并且存儲(chǔ)到了全局Configuration對(duì)象。這樣,在執(zhí)行Mapper接口方法的時(shí)候,根據(jù)上面梳理的執(zhí)行流程中的4、mybatis接口上下文信息MappedStatement,就可以在configuration對(duì)象中取出MappedStatement對(duì)象用于查詢(xún)了,這樣,就整個(gè)串通了@TableFied注解的作用過(guò)程。

4、一些疑問(wèn)

上面梳理了LambdaQuery接口執(zhí)行的過(guò)程以及確定了@TableField注解在這個(gè)過(guò)程中是通過(guò)給字段起別名的方式實(shí)現(xiàn)了數(shù)據(jù)庫(kù)字段和實(shí)體字段的映射。其實(shí)還有幾處疑問(wèn)需要解決

1、為啥手寫(xiě)sql@TableField注解就失效了呢?雖然在三、關(guān)于@TableField注解失效原因的思考中大體上明白了失效的合理性,但是從技術(shù)層面上來(lái)講只是搞明白了內(nèi)置方法對(duì)@TableFied注解的支持,還沒(méi)搞明白手寫(xiě)sql為啥不支持@TableFied注解。再具體點(diǎn),手寫(xiě)sql肯定是沒(méi)有別名的,那它的DynamicSqlSource和內(nèi)置方法的DynamicSqlSource有何不同?手寫(xiě)sql需要定義ResultMap,ResultMap在何時(shí)生效的?退一步說(shuō),手寫(xiě)sql和內(nèi)置方法的查詢(xún)是否走的同一個(gè)查詢(xún)流程呢?

2、使用LambdaQuery的內(nèi)置方法通過(guò)下面的代碼生成MappedStatement對(duì)象并且保存到Configuration全局配置中,手寫(xiě)的sql并不在這個(gè)列表中,手寫(xiě)sql的接口方法何時(shí)創(chuàng)建的MappedStatement對(duì)象的呢?

六、Mapper接口手寫(xiě)sql方法調(diào)用過(guò)程梳理

整個(gè)流程基本上和四、Mapper接口LambdaQuery方法調(diào)用過(guò)程梳理一樣,這里只是說(shuō)下不同之處

1、生成sql的方式不同

在LambdaQuery中,生成sql的方式是使用DynamicSqlSource

其內(nèi)部維護(hù)了一個(gè)rootSqlNode用于解析sql語(yǔ)句,其中查詢(xún)列包含別名被放到了一個(gè)StaticTextSqlNode中;

但是在手寫(xiě)sql的時(shí)候,不再是DynamicSqlSource,而是RawSqlSource:

內(nèi)部不再維護(hù)MixedSqlNode,而是直接使使用一個(gè)sql字符串,該字符串正是xml文件中手寫(xiě)的sql:

<select id="selectOneByName" resultMap="ClientRoleResult">
    select *
    from client_role
    where  name = #{name};
  </select>

很明顯,這里確實(shí)是原生的sql,沒(méi)有任何的mybatis標(biāo)簽混雜在里面。

假如我稍微改一下這段sql又如何?改成如下形式

<select id="selectOneByName" resultMap="ClientRoleResult">
    select *
    from client_role
    <where>
      name = #{name};
    </where>
  </select>

兩段代碼邏輯上是完全一樣的,再次運(yùn)行debug到此處

可以看到,sqlSource已經(jīng)變成了DynamicSqlSource,只是它相對(duì)于LambdaQuery的查詢(xún)方式,少了很多個(gè)SqlNode節(jié)點(diǎn)。雖然變成了DynamicSqlSource,但是可以看到還是沒(méi)有設(shè)置別名,StaticTextSqlNode中存儲(chǔ)了xml文件中寫(xiě)的原始的sql字符串。

這樣可以得出結(jié)論:如果xml文件中寫(xiě)的sql沒(méi)有使用任何mybatis的標(biāo)簽,則會(huì)使用RawSqlSource,如果使用了例如<where></where>等標(biāo)簽,則會(huì)使用DynamicSqlSource;同樣使用的都是DynamicSqlSource的情況下,手寫(xiě)Sql的DynamicSqlSource查詢(xún)列不會(huì)自動(dòng)增加別名,查詢(xún)列取決于手寫(xiě)sql的代碼。

需要注意的是執(zhí)行這段代碼的是org.apache.ibatis.mapping.MappedStatement對(duì)象,它是在服務(wù)啟動(dòng)的時(shí)候創(chuàng)建并保存到全局MybatisConfiguration中的,也就是說(shuō),在服務(wù)啟動(dòng)的時(shí)候就已經(jīng)決定了在這里查詢(xún)的時(shí)候使用的是DynamicSqlSource還是RawSqlSource。

2、結(jié)果集處理方式不同

之前說(shuō)過(guò),即使是查詢(xún)一個(gè)元素,底層還是會(huì)查詢(xún)List,然后對(duì)每個(gè)元素單獨(dú)反序列化封裝成實(shí)體類(lèi)對(duì)象,這個(gè)操作在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查詢(xún)的時(shí)候,402行代碼返回的foundValues值為true,方法執(zhí)行完成,rowValue就有值了,見(jiàn)下圖:

404行的applyPropertyMappings方法執(zhí)行則會(huì)直接跳過(guò)執(zhí)行,因?yàn)椴粷M足執(zhí)行條件;

而當(dāng)手寫(xiě)sql方法調(diào)用時(shí),402行的applyAutomaticMappings方法執(zhí)行會(huì)返回false,執(zhí)行完成之后rowValue字段屬性并沒(méi)有填充,見(jiàn)下圖:

而404行的applyPropertyMappings方法滿足了執(zhí)行條件,執(zhí)行完成之后foundValues的值變成了true,而rawValue也有值了。

為啥呢?

applyAutomaticMappings方法和applyPropertyMappings方法兩個(gè)方法從方法名字上來(lái)看似乎是對(duì)立的兩個(gè)方法如果未指定PropertieyMapping,則走applyAutomacitMapping,如果指定了則走applyPropertyMapping,但是會(huì)不會(huì)同時(shí)存在兩個(gè)方法都走一遍呢?那是肯定的,因?yàn)閍pplyPropertyMapping并沒(méi)有放在else塊中,它是強(qiáng)制執(zhí)行的,為了驗(yàn)證這個(gè)問(wèn)題,修改下Xml文件中定義的ResultMap,原來(lái)ResultMap長(zhǎng)這樣子

 <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)在我改成這個(gè)樣子

  <resultMap type="com.kdyzm.demo.springboot.entity.ClientRole" id="ClientRoleResult">
    <result property="description" column="desc"/>
  </resultMap>

刪掉表字段和實(shí)體字段同名的映射關(guān)系,只留下不同的映射關(guān)系,再次執(zhí)行手寫(xiě)sql的接口查詢(xún)。

執(zhí)行完成applyAutomaticMappings方法之后,未在ResultMap中指定映射關(guān)系的id和name兩個(gè)屬性填充上了值,如下圖:

執(zhí)行完成applyPropertyMappings方法之后,在ResultMap中定了映射關(guān)系的description字段填充上了值,如下圖:

說(shuō)明了一個(gè)問(wèn)題:只要在ResultMap中沒(méi)定義映射關(guān)系,就會(huì)被applyAutomaticMappings方法處理屬性填充;如果在ResultMap中定義了映射關(guān)系,則會(huì)被applyPropertyMappings方法處理屬性填充;另外,說(shuō)明了ResultMap不需要全部都寫(xiě)上關(guān)系映射,只需要寫(xiě)數(shù)據(jù)庫(kù)字段名和實(shí)體類(lèi)字段不一致的映射即可。

那么如何區(qū)分出來(lái)哪些字段該走applyAutomaticMappings方法屬性填充,哪些字段該走applyPropertyMappings屬性填充呢?

答案就在傳過(guò)來(lái)的resultMap對(duì)象中,它有個(gè)屬性叫ResultMapping,存儲(chǔ)著解析XML文件中ResultMap的映射,如下圖所示:

凡是在resultMapping中的屬性,都走applyPropertyMappings方法,否則走applyAutomaticMappings方法。

3、手寫(xiě)sql接口方法@TableFied注解失效的原因

一開(kāi)始未在xml文件中定義ResultMapping,且使用的是手寫(xiě)sql。根據(jù)上面的源碼分析,在未定義ResultMap的情況下,所有的屬性填充都會(huì)走org.apache.ibatis.executor.resultset.DefaultResultSetHandler#applyAutomaticMappings方法,其邏輯也比較清晰

首先查找出所有未在xml文件中定義的ResultMap映射表字段集合對(duì)這些表字段進(jìn)行處理,比如如果開(kāi)啟了mapUnderscoreToCamelCase,則會(huì)將表字段從下?lián)Q線變成駝峰命名嘗試從實(shí)體類(lèi)中尋找轉(zhuǎn)換好的字段,如果找到了,則全部放到List<UnMappedColumnAutoMapping> autoMapping從mapping尋找適合的typeHandler解析屬性值,比如Long類(lèi)型的值會(huì)調(diào)用LongTypeHandler進(jìn)行屬性值解析屬性值填充到rawValue

套用上述流程,看看description字段為啥沒(méi)填充上去:

  • 首先查找出所有未在xml文件中定義的ResultMap映射表字段集合,找到了id,name,desc三個(gè)表字段
  • 對(duì)這些表字段進(jìn)行處理,比如如果開(kāi)啟了mapUnderscoreToCamelCase,則會(huì)將表字段從下?lián)Q線變成駝峰命名,三個(gè)字段都無(wú)變化
  • 嘗試從實(shí)體類(lèi)尋找轉(zhuǎn)換好的字段,如果找到了,則全部放到List<UnMappedColumnAutoMapping> autoMapping,實(shí)體類(lèi)有三個(gè)字段id,name,description,id,name都找到了,由于desc和description長(zhǎng)得不一樣,所以就沒(méi)填充到List<UnMappedColumnAutoMapping> autoMapping,最終上圖中只有id和name兩個(gè)屬性值被add到了autoMapping。從mapping尋找適合的typeHandler解析
  • 屬性值,這里只解析了id和name兩個(gè)字段的屬性值屬性值填充到rawValue,這里只填充了id和name兩個(gè)字段的屬性值

總結(jié)下,desc字段因?yàn)闆](méi)有在ResultMap中定義,所以不會(huì)被applyPropertyMappings方法處理;本來(lái)應(yīng)該被applyAutomaticMappings處理的,又因?yàn)楹蚫escription實(shí)體類(lèi)字段名長(zhǎng)得不一樣,就被applyAutomaticMappings方法忽略了,成了一個(gè)兩不管的狀態(tài),所以最終只能是默認(rèn)值填充,那就是null了。

那么@TableFied字段真的一點(diǎn)用就沒(méi)了嗎,上述流程中代碼中怎么知道數(shù)據(jù)庫(kù)表字段的呢?

表字段都被封裝到了ResultSetWrapper對(duì)象中,如下圖所示

這些表字段是從執(zhí)行結(jié)果ResultSet中的元數(shù)據(jù)獲取到的,最終通過(guò)構(gòu)造方法填充屬性值,如下圖所示

所以,當(dāng)手寫(xiě)sql的時(shí)候,@TableField注解就真的完全沒(méi)用了。

下面說(shuō)下手寫(xiě)sqlmapper方法創(chuàng)建對(duì)應(yīng)MappedStatement對(duì)象的過(guò)程。

4、手寫(xiě)SQL的MappedStatement對(duì)象的創(chuàng)建

同樣的,手寫(xiě)sql的MappedStatement對(duì)象的創(chuàng)建也是在SqlSessionFactoryBean對(duì)象創(chuàng)建的過(guò)程中創(chuàng)建的。但是手寫(xiě)SQL的MappedStatement對(duì)象創(chuàng)建的時(shí)間遠(yuǎn)比mybatis plus內(nèi)置方法的創(chuàng)建早的多。

創(chuàng)建SqlSessionFactoryBean的入口方法:com.baomidou.mybatisplus.extension.spring.MybatisSqlSessionFactoryBean#buildSqlSessionFactory

這段代碼會(huì)解析所有的xml文件并且最終在org.apache.ibatis.builder.MapperBuilderAssistant#addMappedStatement方法中創(chuàng)建手寫(xiě)sql的MapperStatement并保存到Configuration上下文中。

七、手寫(xiě)SQL如何讓@TableFiled生效

如果,我就是想手寫(xiě)SQL,還不想寫(xiě)ResultMap而且還想@TableField注解生效,又該怎么做呢?

先說(shuō)下結(jié)論:理論上可行,實(shí)踐很困難。下面逐一分析各種方法的可行性。

1、方法一:新增ResultMapping

通過(guò)上面的源碼分析,知道了mybatis針對(duì)每個(gè)Mapper接口都創(chuàng)建了一個(gè)MappedStatement對(duì)象,該對(duì)象實(shí)際上存儲(chǔ)了該接口的上下文信息,無(wú)論是執(zhí)行的sql還是結(jié)果類(lèi)型、字段Mapping等都在里面(不包含ResultSet返回的行動(dòng)態(tài)AutoMapping),在反序列化之前修改該對(duì)象,根據(jù)@TableFied注解新增數(shù)據(jù)庫(kù)字段和實(shí)體類(lèi)字段的映射關(guān)系,就應(yīng)該能影響反序列化結(jié)果。

然而,我發(fā)現(xiàn)所有相關(guān)的屬性都被修飾成了不可修改的集合,這里有個(gè)最關(guān)鍵的resultMappings集合,也被修飾成了不可修改的集合,看起來(lái)官方并不想我們動(dòng)他們的數(shù)據(jù),畢竟萬(wàn)一出了問(wèn)題,就很難排查是誰(shuí)導(dǎo)致的了。

所以說(shuō),這種方式行不通。

2、方法二:使用插件填充未被設(shè)置值的屬性

如果沒(méi)設(shè)置ResultMap,會(huì)使用自動(dòng)映射的方式填充實(shí)體類(lèi)對(duì)象,desc和descriptin字段的映射則會(huì)失敗,最終到實(shí)體類(lèi)對(duì)象里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 {
        //通過(guò)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;
}

代碼寫(xiě)到上述TODO的地方就寫(xiě)不下去了。。。原因是Satement對(duì)象中的結(jié)果只能讀一次,在第一次List result = (List) invocation.proceed();執(zhí)行過(guò)后,再次取結(jié)果就取不出來(lái)了。

而且coding的過(guò)程中發(fā)現(xiàn)其它的問(wèn)題:MappedStatement對(duì)象作為DefaultResultSetHandler的成員變量并沒(méi)有暴露GET/SET方法,要想獲取到必須通過(guò)反射暴力獲?。?/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;
    }

在我感覺(jué)其實(shí)很不爽,畢竟強(qiáng)扭的瓜不甜。。。

總而言之,這種方式也以失敗告終,那只能用最后一種終極方法了:自定義反序列化的過(guò)程。

3、方法三:自定義反序列化過(guò)程

這種方式確實(shí)可以實(shí)現(xiàn),但是實(shí)現(xiàn)起來(lái)會(huì)很困難,因?yàn)椴幌肫茐膍ybatis和mybaits plus原有的功能,比如:autoMapping、下劃線轉(zhuǎn)駝峰、resultMap、各種返回類(lèi)型處理。。。如果自己重新實(shí)現(xiàn),代價(jià)就太大了,這是得不償失的做法。如果不破壞這些功能,只是稍微做些修改的話是可以接受的。

4、方法四:增強(qiáng)反序列化過(guò)程

首先制定一個(gè)反序列化的規(guī)則:當(dāng)手寫(xiě)sql的時(shí)候,自動(dòng)mapping和resultmap優(yōu)先級(jí)最高,之后若是有未匹配的屬性,則使用@TableField注解嘗試解決,最終如果還是無(wú)法匹配,則直接pass掉不做處理。

這里處理的核心方法就是在mybatis反序列化處理完單個(gè)對(duì)象之后額外添加邏輯,核心方法就在:DefaultResultSetHandler#getRowValue(org.apache.ibatis.executor.resultset.ResultSetWrapper, org.apache.ibatis.mapping.ResultMap, java.lang.String)方法中

問(wèn)題就在于此處代碼無(wú)法應(yīng)用動(dòng)態(tài)代理或者切面技術(shù),最終,我使用了javassit技術(shù)動(dòng)態(tài)修改字節(jié)碼對(duì)象解決了該問(wèn)題,javassit簡(jiǎn)介可以參考文章:使用javassist運(yùn)行時(shí)動(dòng)態(tài)修改字節(jié)碼對(duì)象

項(xiàng)目源代碼地址:狂盜一枝梅 / mybatis-plus-fix

最終,實(shí)現(xiàn)效果上來(lái)看,確實(shí)解決了@TableFiled注解在手寫(xiě)sql的情況下失效的問(wèn)題,但是由于額外執(zhí)行了一段代碼,所以執(zhí)行效率會(huì)稍微低一些;而且由于使用了javassit,代碼的可讀性和可維護(hù)性較低,尤其是在debug代碼的時(shí)候會(huì)出現(xiàn)靈異現(xiàn)象。。。綜上,作為實(shí)驗(yàn)性的問(wèn)題解決,雖然能解決問(wèn)題,但是不建議使用,哈哈

到此這篇關(guān)于mybatis plus框架的@TableField注解不生效問(wèn)題總結(jié)的文章就介紹到這了,更多相關(guān)mybatis plus @TableField注解不生效內(nèi)容請(qǐng)搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關(guān)文章希望大家以后多多支持腳本之家!

相關(guān)文章

  • Spring Boot 實(shí)現(xiàn)圖片上傳并回顯功能

    Spring Boot 實(shí)現(xiàn)圖片上傳并回顯功能

    本篇文章給大家分享Spring Boot 實(shí)現(xiàn)圖片上傳并回顯功能,文中通過(guò)實(shí)例代碼給大家介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或工作具有一定的參考借鑒價(jià)值,需要的朋友參考下吧
    2021-07-07
  • Java中異常傳播的實(shí)現(xiàn)

    Java中異常傳播的實(shí)現(xiàn)

    在Java中,異常傳播是一個(gè)重要的概念,本文主要介紹了Java中異常傳播的實(shí)現(xiàn),具有一定的參考價(jià)值,感興趣的可以了解一下
    2024-01-01
  • Java數(shù)據(jù)類(lèi)型超詳細(xì)示例講解

    Java數(shù)據(jù)類(lèi)型超詳細(xì)示例講解

    Java程序中要求參與的計(jì)算的數(shù)據(jù),必須要保證數(shù)據(jù)類(lèi)型的一致性,如果數(shù)據(jù)類(lèi)型不一致將發(fā)生類(lèi)型的轉(zhuǎn)換。本文將通過(guò)示例詳細(xì)說(shuō)說(shuō)Java中數(shù)據(jù)類(lèi)型的轉(zhuǎn)換,感興趣的可以了解一下
    2022-11-11
  • 最新評(píng)論