admin 发表于 2022-3-4 19:45:37

mybatis plus框架的@TableField注解不生效问题总结

一、问题描述
最近遇到一个mybatis plus的问题,@TableField注解不生效,导致查出来的字段反序列化后为空

数据库表结构:
CREATE TABLE `client_role` (
`id` int(11) NOT NULL AUTO_INCREMENT COMMENT '自增主键',
`name` varchar(64) NOT NULL COMMENT '角色的唯一标识',
`desc` varchar(64) DEFAULT NULL COMMENT '角色描述',
PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='角色表'

对应的实体类
@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;

    /**
   * 角色的唯一标识
   */
    @NotEmpty
    @ApiModelProperty(value = "角色的唯一标识")
    @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")
);

真是活见鬼,两种方法理论上结果应该是一模一样的,最终却发现@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标签中resultType改成resultMap,值为resultMap标签的id,这样description字段就有值了。

问题很容易解决,但是有个问题需要问下为什么:为什么@TableField注解在手写sql的时候就失效了呢?

三、关于@TableField注解失效原因的思考
当数据库字段和自定义的实体类中字段名不一致的时候,可以使用@TableField注解实现矫正,以上面的代码为例
ClientRole admin = iClientRoleMapper.selectOne(
    new LambdaQueryWrapper<ClientRole>().eq(ClientRole::getName, "admin")
);

这段代码被翻译成sql,它被翻译成这样

好家伙,原来@TableField注解功能是通过加别名实现的。

那如果是手写sql的话,它如何把别名加上去呢?答案就是没办法加上去,因为手写sql太灵活了,不在mybatis plus功能框架内,那是属于原生mybatis的功能范畴,不支持也就正常了。

四、Mapper接口LambdaQuery方法调用过程梳理
进一步探讨,@TableField注解是如何生成别名的呢,那就要研究下源码了。

1、Mapper接口调用实际上使用的是动态代理技术
mybatis定义的都是一堆的接口,并没有实现类,但是却能正常调用,这很明显使用了动态代理技术,实际上注入spring的时候接口被包装成了代理对象,这就为debug源码提供了突破口。


可以看到,这个代理对象实际的类名为com.baomidou.mybatisplus.core.override.MybatisMapperProxy,它实现了InvocationHandler接口,确定是JDK动态代理无疑了,那么所有的逻辑都会走com.baomidou.mybatisplus.core.override.MybatisMapperProxy#invoke方法

2、mybatis plus对查询的单独处理
根据上面一步找到源码的入口,一步一步走下去,接口调用到了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的相应的方法调用,而对于查询请求,则逻辑比较复杂,毕竟sql最复杂的地方就是查询了;还有另外一个特点,针对不同的返回结果类型,也走不同的逻辑;由于我这里查询返回的是一个实体对象,所以最终走到了如下断点

从代码上来看,也只是委托给了SqlSessionTemplate对象处理了,然而SqlSessionTemplate的全包名是org.mybatis.spring.SqlSessionTemplate,它是mybatis集成spring的官方功能,和mybatis plus没关系,就这如何能让@TableField注解发挥作用呢?

3、findOne实际上还是要查询List
继续debug几次,到了一个有趣的方法org.apache.ibatis.session.defaults.DefaultSqlSession#selectOne(java.lang.String, java.lang.Object)

原来单独查询一个对象,还是要查询List,然后取出第一个对象返回;如果查询出多个对象,则直接抛出TooManyResultsException,建表的时候不做唯一索引查出来多个对象的时候抛出的异常就是在这里做的。

有意思的是,方法执行到这里,传参只有两个,一个是方法名,另外一个是查询参数

总之还是要继续查看selectList的逻辑,才能搞清楚逻辑

4、mybatis接口上下文信息MappedStatement
上一步说到selectList方法调用只传递了两个参数,一个是方法名,一个是方法参数,只是这两个参数是无法满足查询的请求的,毕竟最重要的sql语句都没传,debug下去,到了一处比较重要的地方,就解开了我的疑问:org.apache.ibatis.session.defaults.DefaultSqlSession#selectList(java.lang.String, java.lang.Object, org.apache.ibatis.session.RowBounds)

在这个方法里,根据statement也就是方法名获取到了MappedStatement对象,这个对象里存储着这个关于本次查询需要的上下文信息,继续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)

它调用了MappedStatement对象的getBoundSql方法,便得到了带有别名的sql字符串,也就是说,这个getBoundSql方法形成了这段sql字符串,debug进去看看

5、mybatis plus别名自动设置的逻辑
debug ms.getBoundSql方法,最终到了方法:org.apache.ibatis.scripting.xmltags.MixedSqlNode#apply,该方法入参是org.apache.ibatis.scripting.xmltags.DynamicContext类型,其内部维护了一个java.util.StringJoiner对象,专门用于拼接sql

contents对象是个List类表,其有八个元素,经过八个元素的apply方法调用之后,DynamicContext的sqlBuilder对象就有了值了

原来别名是在这里设置的;这里先暂且不谈,查询流程还没结束,先看整个的流程。

6、mybatis plus的sql日志打印
我们看到的sql日志是如何打印出来的?上一步已经获取到了sql,接下来继续debug,就会看到sql打印的代码:org.apache.ibatis.logging.jdbc.ConnectionLogger#invoke

7、最终查询的执行
我们知道,无论是mybatis还是其它框架,最终执行查询都要遵循java api规范,上一步已经获取到了PreparedStatement,最终在这个方法执行了查询

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

8、结果集处理
查询完之后要封装结果集,封装逻辑的起始方法:org.apache.ibatis.executor.resultset.DefaultResultSetHandler#handleResultSets

可以看到,这段逻辑就是在从Satement对象中循环取数据,然后调用org.apache.ibatis.executor.resultset.DefaultResultSetHandler#handleResultSet方法处理每一条数据

9、每一条数据的单独处理
继续debug,可以看到对每一条结果数据的单独处理的逻辑:org.apache.ibatis.executor.resultset.DefaultResultSetHandler#getRowValue(org.apache.ibatis.executor.resultset.ResultSetWrapper, org.apache.ibatis.mapping.ResultMap, java.lang.String)

这里首先使用自动字段名映射的方式填充返回值,然后使用resultMap继续填充返回值,最后返回rowValue作为最终反序列化完成的值。

至此,整个查询过程基本上就结束了。

五、@TableField注解生效原理
1、别名sql在mapper方法执行前就已经确定
上一步在梳理Mapper接口调用过程的时候在第5点说过,DynamicContext内部维护了一个StringJoiner对象用于拼接sql,在经过MixedSqlNode内部的8个SqlNode处理之后,StringJoiner就有了完整的sql语句。我们知道@TableField生效的原理是设置别名,那么别名是这时候设置上去的吗?

页: [1]
查看完整版本: mybatis plus框架的@TableField注解不生效问题总结