php中文网 | cnphp.com

 找回密码
 立即注册

QQ登录

只需一步,快速开始

搜索
查看: 718|回复: 0

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

[复制链接]

3142

主题

3152

帖子

1万

积分

管理员

Rank: 9Rank: 9Rank: 9

UID
1
威望
0
积分
7956
贡献
0
注册时间
2021-4-14
最后登录
2024-11-22
在线时间
763 小时
QQ
发表于 2022-3-4 19:45:37 | 显示全部楼层 |阅读模式
一、问题描述
最近遇到一个mybatis plus的问题,@TableField注解不生效,导致查出来的字段反序列化后为空

数据库表结构:
[mw_shl_code=sql,true]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='角色表'
[/mw_shl_code]
对应的实体类
[mw_shl_code=java,true]@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;

}
[/mw_shl_code]
就是description字段为空的问题,查询sql如下
[mw_shl_code=applescript,true]  <select id="selectOneByName" resultType="com.kdyzm.demo.springboot.entity.ClientRole">
    select *
    from client_role
    where name = #{name};
  </select>
[/mw_shl_code]
然而,如果不手写sql,使用mybatis plus自带的LambdaQuery查询,则description字段就有值了。
[mw_shl_code=applescript,true]ClientRole admin = iClientRoleMapper.selectOne(
    new LambdaQueryWrapper<ClientRole>().eq(ClientRole::getName, "admin")
);
[/mw_shl_code]
真是活见鬼,两种方法理论上结果应该是一模一样的,最终却发现@TableField字段在手写sql这种方式下失效了。

二、解决方案
定义ResultMap,在xml文件中定义如下
[mw_shl_code=applescript,true]  <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>
[/mw_shl_code]
select标签中resultType改成resultMap,值为resultMap标签的id,这样description字段就有值了。

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

三、关于@TableField注解失效原因的思考
当数据库字段和自定义的实体类中字段名不一致的时候,可以使用@TableField注解实现矫正,以上面的代码为例
[mw_shl_code=applescript,true]ClientRole admin = iClientRoleMapper.selectOne(
    new LambdaQueryWrapper<ClientRole>().eq(ClientRole::getName, "admin")
);
[/mw_shl_code]
这段代码被翻译成sql,它被翻译成这样
image.png
好家伙,原来@TableField注解功能是通过加别名实现的。

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

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

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

image.png
可以看到,这个代理对象实际的类名为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方法
[mw_shl_code=applescript,true]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;
    }
[/mw_shl_code]
这段代码特点在于它对于非查询类型的请求(比如插入、更新和删除),都直接委托给了sqlSeesion的相应的方法调用,而对于查询请求,则逻辑比较复杂,毕竟sql最复杂的地方就是查询了;还有另外一个特点,针对不同的返回结果类型,也走不同的逻辑;由于我这里查询返回的是一个实体对象,所以最终走到了如下断点
image.png
从代码上来看,也只是委托给了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)
image.png
原来单独查询一个对象,还是要查询List,然后取出第一个对象返回;如果查询出多个对象,则直接抛出TooManyResultsException,建表的时候不做唯一索引查出来多个对象的时候抛出的异常就是在这里做的。

有意思的是,方法执行到这里,传参只有两个,一个是方法名,另外一个是查询参数
image.png
总之还是要继续查看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)
image.png
在这个方法里,根据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)
image.png
它调用了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
image.png
contents对象是个List类表,其有八个元素,经过八个元素的apply方法调用之后,DynamicContext的sqlBuilder对象就有了值了
image.png
原来别名是在这里设置的;这里先暂且不谈,查询流程还没结束,先看整个的流程。

6、mybatis plus的sql日志打印
我们看到的sql日志是如何打印出来的?上一步已经获取到了sql,接下来继续debug,就会看到sql打印的代码:org.apache.ibatis.logging.jdbc.ConnectionLogger#invoke
image.png
7、最终查询的执行
我们知道,无论是mybatis还是其它框架,最终执行查询都要遵循java api规范,上一步已经获取到了PreparedStatement,最终在这个方法执行了查询

org.apache.ibatis.executor.statement.PreparedStatementHandler#query
image.png
8、结果集处理
查询完之后要封装结果集,封装逻辑的起始方法:org.apache.ibatis.executor.resultset.DefaultResultSetHandler#handleResultSets
image.png
可以看到,这段逻辑就是在从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)
image.png
这里首先使用自动字段名映射的方式填充返回值,然后使用resultMap继续填充返回值,最后返回rowValue作为最终反序列化完成的值。

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

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





上一篇:用 UI 多线程处理 WPF 大量渲染的解决方案
下一篇:什么是NFT?
回复

使用道具 举报

您需要登录后才可以回帖 登录 | 立即注册

本版积分规则

QQ|php中文网 | cnphp.com ( 赣ICP备2021002321号-2 )

GMT+8, 2024-11-22 09:02 , Processed in 0.275645 second(s), 37 queries , Gzip On.

Powered by Discuz! X3.4 Licensed

Copyright © 2001-2020, Tencent Cloud.

申明:本站所有资源皆搜集自网络,相关版权归版权持有人所有,如有侵权,请电邮(fiorkn@foxmail.com)告之,本站会尽快删除。

快速回复 返回顶部 返回列表