MyBatis布尔字段映射陷阱全过程解析

在开发过程中,我们常常会遇到一些看似简单却令人困惑的问题。本文记录了一次将 boolean 改为 Boolean 后,MyBatis 插入数据时出现的意外情况。本文不仅逐步揭示了问题的根本原因,还提供了解决方案,并强调了在开发中遵循规范和仔细排查问题的重要性。

背景

为了实现某个功能,需要为已有的表新增字段,其中有一个字段需要表达的含义是:是否有对话条数。

加字段要遵守规范,咱就去看了《阿里巴巴开发规约》的"MySQL规约",有这么一段描述:

因此,**"是否有对话条数"**的字段名为 is_has_messages,数据类型为:unsigned tinyint(1表示是,0表示否;默认为1)

给mysql加好字段了,咱还得给 xxxDO 加上字段,按照上面的说法"POJO类中的任何布尔类型的变量,都不要加is前缀",那就这么写:

typescript 复制代码
/**
 * 是否有对话条数;1表示是,0表示否 <br>
 * 默认为1
 */
private boolean hasMessages;

一切看起来是那么的自然~

go 复制代码
翻车
奇怪的结果

is_has_messages咋是0了?true 不应该映射为1吗?

各种检查代码,都没找到原因... 因为相关业务逻辑太简单了,1 + 1 = 2,有啥需要怀疑的吗?

打印sql
  • 拦截器
java 复制代码
/**
 * @author hanxu 
 * @mail 
 * @since 2024/12/4
 */
@Slf4j
@Intercepts({
    @Signature(type = StatementHandler.class, method = "prepare", args = {Connection.class, Integer.class}),
})
public class MyBatisSqlInterceptor implements Interceptor {


    @Override
    public Object intercept(Invocation invocation) throws Throwable {
        StatementHandler statementHandler = (StatementHandler) invocation.getTarget();
        // 获取SQL语句
        String sql = statementHandler.getBoundSql().getSql();
        // 获取参数
        Object parameterObject = statementHandler.getBoundSql().getParameterObject();


        log.info("Executing SQL: {}", sql);
        log.info("Parameters: {}", parameterObject);


        // 执行原始方法调用
        return invocation.proceed();
    }


    @Override
    public Object plugin(Object target) {
        return Plugin.wrap(target, this);
    }


    @Override
    public void setProperties(Properties properties) {
        // 如果需要可以读取配置
    }
}
xml 复制代码
<?xml version="1.0" encoding="UTF-8" ?>
<!-- mybatis的配置文件 -->
<!DOCTYPE configuration
PUBLIC "-//mybatis.org//DTD Config 3.0//EN"
"http://mybatis.org/dtd/mybatis-3-config.dtd">
<configuration>
  <settings>
    ...
  </settings>
  <plugins>
    ...
    <plugin interceptor="com.alibaba.xxx.utils.mybatis.interceptor.MyBatisSqlInterceptor"/>
  </plugins>
</configuration>
xml 复制代码
<?xml version="1.0" encoding="UTF-8"?>
<configuration>
  ...


  <appender name="CONSOLE" class="ch.qos.logback.core.ConsoleAppender">
    <encoder>
      <pattern>${CONSOLE_LOG_PATTERN}</pattern>
      <charset>utf8</charset>
    </encoder>
  </appender>


  <!--  控制台打印dao层日志  -->
  <logger name="com.alibaba.xxx.dao" level="DEBUG" additivity="false">
    <appender-ref ref="CONSOLE" />
  </logger>


  ...
</configuration>

  • 日志

通过拦截器把sql打印出来,也没发现啥异常的地方:

swift 复制代码
c.a.i.u.m.i.MyBatisSqlInterceptor - Parameters: xxxDO(empId=xxx, stageName=xxx, realName=xxx, summary=利物浦的首都是?, status=NORMAL, feature={"talkType":"MODEL"}, sessionId=f703babf-791b-432b-a162-7e5d4731bbca, talkEntityType=MODEL, setAsTop=false, hasMessages=true)


c.a.i.u.m.i.MyBatisSqlInterceptor - Executing SQL: INSERT INTO
         
        xxx_session
     
        ( 
        gmt_create,
        gmt_modified,
        emp_id,
        stage_name,
        real_name,
        summary,
        feature,
        status,
        session_id,
        talk_entity_type,
        is_set_as_up,
        is_has_messages
     )
        VALUES
        ( 
        now(),
        now(),
        ?,
        ?,
        ?,
        ?,
        ?,
        ?,
        ?,
        ?,
        ?,
        ?
     )
go 复制代码
启发

isHasMessages(): boolean

难道说,mybatis实际上调用的是getHasMessages()?

MyBatis 会查找参数对象中是否存在 getHasMessages() 方法或者 hasMessages 字段。

由于压根就没有 getHasMessages() 方法,因此使用了 hasMessages 字段的值,也就是默认的false。

如此一来,落db后,is_has_messages正好是0。

可以确认的是,落db的时候,#{hasMessages}一定不是缺失的。因为我给is_has_messages配了默认值为1,一旦缺失了,db里的结果应该为1,而不是0。

实践

从上文可知,我们想知道is_has_messages的占位符到底是什么值。如果是true,那么落到db里的一定是1,反之则为0。

ParameterHandler在Mybatis中负责将sql中的占位符替换为真正的参数。其只有一个实现类:DefaultParameterHandler。

  • 我们重点debug:DefaultParameterHandler的setParameters方法
java 复制代码
...


/**
 * @author Clinton Begin
 * @author Eduardo Macarron
 */
public class DefaultParameterHandler implements ParameterHandler {


    private final TypeHandlerRegistry typeHandlerRegistry;


    private final MappedStatement mappedStatement;
    private final Object parameterObject;
    private final BoundSql boundSql;
    private final Configuration configuration;


    public DefaultParameterHandler(MappedStatement mappedStatement, Object parameterObject, BoundSql boundSql) {
        this.mappedStatement = mappedStatement;
        this.configuration = mappedStatement.getConfiguration();
        this.typeHandlerRegistry = mappedStatement.getConfiguration().getTypeHandlerRegistry();
        this.parameterObject = parameterObject;
        this.boundSql = boundSql;
    }


    @Override
    public Object getParameterObject() {
        return parameterObject;
    }


    @Override
    public void setParameters(PreparedStatement ps) {
        ErrorContext.instance().activity("setting parameters").object(mappedStatement.getParameterMap().getId());
        List<ParameterMapping> parameterMappings = boundSql.getParameterMappings();
        if (parameterMappings != null) {
            for (int i = 0; i < parameterMappings.size(); i++) {
                ParameterMapping parameterMapping = parameterMappings.get(i);
                if (parameterMapping.getMode() != ParameterMode.OUT) {
                    Object value;
                    String propertyName = parameterMapping.getProperty();
                    if (boundSql.hasAdditionalParameter(propertyName)) { // issue #448 ask first for additional params
                        value = boundSql.getAdditionalParameter(propertyName);
                    } else if (parameterObject == null) {
                        value = null;
                    } else if (typeHandlerRegistry.hasTypeHandler(parameterObject.getClass())) {
                        value = parameterObject;
                    } else {
                        MetaObject metaObject = configuration.newMetaObject(parameterObject);
                        value = metaObject.getValue(propertyName);
                    }
                    TypeHandler typeHandler = parameterMapping.getTypeHandler();
                    JdbcType jdbcType = parameterMapping.getJdbcType();
                    if (value == null && jdbcType == null) {
                        jdbcType = configuration.getJdbcTypeForNull();
                    }
                    try {
                        typeHandler.setParameter(ps, i + 1, value, jdbcType);
                    } catch (TypeException | SQLException e) {
                        throw new TypeException("Could not set parameters for mapping: " + parameterMapping + ". Cause: " + e, e);
                    }
                }
            }
        }
    }


}
isHasMessages方法也是可以的...
计算机不存在了... is_has_messages又是0了...

尾声

再折腾,需求做不完了... 我知道的解决办法是:

复盘

前几日早晨,被猫搞醒了,突然间灵光一现,想明白"智子"到底是谁了。收拾整顿后,便来验证自己的想法了。

给自己,也给读者朋友们一个答复。毕竟没有结论的文章,就像说一半的话,让人难受。

还原现场
抓出真凶

我们的注意力都放在addSession接口了,在debug了Mybatis的源码时,非常肯定确认插入db的时候,is_has_messages的值一定是true。为什么有时候is_has_messages的值一会儿是1,一会儿又是0呢?

那是因为有一只被忽视的手(或者说"智子")在暗中操作,没被我们看见。这只手便是xxxTalk接口。

  • xxxTalk接口为啥要去update呢?因为用户对话后,要刷新session表的更新时间,使得用户正在对话的session在session列表中处于第一个。

我们看一下:updateById的写法:

当xxxSessionDO的hasMessages的类型为boolean时,默认是false。

因此,updateById时, is_has_messages为0。

猜想

未debug时,addSession接口比xxxTalk接口执行快,addSession接口insert时,is_has_messages为1;但xxxTalk接口update后,把is_has_messages修改为0。

debug时,addSession接口阻塞了,xxxTalk接口update无效(因为还没这条记录);addSession接口insert时,is_has_messages为1。

这就是为什么我们一会儿能看到is_has_messages为1,一会儿又为0。

验证

如果我的猜想是对的,那么我们注释掉"xxxTalk接口update"的代码,应该能看到is_has_messages为1的结果。

果真如此,猜想正确!这背后的"智子"便是:xxxTalk接口update了xxx_session表。

当我们将hasMessages的boolean改成Boolean时,我们还做了一件非常重要的事情:将hasMessages默认为TRUE。这样即使"xxxTalk接口update",is_has_messages还是1。

typescript 复制代码
private Boolean hasMessages = Boolean.TRUE;

当然了,最后的解法不是注释掉"xxxTalk接口update"的代码,而是:

go 复制代码
结语

在这篇文章中,我们经历了一次从困惑到柳暗花明的过程。最终,我们将boolean改为Boolean,并显式地设置了默认值为TRUE,成功解决了问题。这一过程不仅让我们更加了解MyBatis的参数处理机制,也提醒我们在开发中要特别注意类型的选择和默认值的设置,避免因细微的差异而导致意想不到的错误。

相信科学,保持好奇心,勇于探索问题的本质,才能在面对复杂的技术挑战时找到正确的解决方案。希望这篇文章能为各位开发者提供一些有价值的参考,帮助大家在未来的开发中少走弯路。

团队介绍

我们是淘天集团-ideaLAB团队,专注于运用AI工程技术提升集团内部业务的生产效率。我们深入研究PE、RAG、Agent、多模态及模型评测等前沿领域,并积极参与集团内ideaLAB产品的建设。我们的目标是通过开发AI SDK和一站式AI Studio,提供先进的AI应用构建能力。同时,我们致力于将AI工程技术广泛应用于集团的多种业务场景,包括但不限于toC、toB和toP等领域,借助丰富的实践经验,不断探索和实施创新技术,以实现更智能、更高效的业务解决方案。

¤ 拓展阅读 ¤

3DXR技术 | 终端技术 | 音视频技术

服务端技术 | 技术质量 | 数据算法

相关推荐
NiNg_1_2341 小时前
MyBatis和JPA区别详解
mybatis·jpa
LuiChun2 小时前
webview_flutter_android 4.3.0使用
android·flutter
Tanecious.2 小时前
C语言--分支循环实践:猜数字游戏
android·c语言·游戏
闲暇部落3 小时前
kotlin内联函数——takeIf和takeUnless
android·kotlin
huang_hai_an8 小时前
MyBatis 写法
mybatis
Android西红柿13 小时前
flutter-android混合编译,原生接入
android·flutter
中國移动丶移不动13 小时前
Java 反射与动态代理:实践中的应用与陷阱
java·spring boot·后端·spring·mybatis·hibernate
大叔编程奋斗记13 小时前
【Salesforce】审批流程,代理登录 tips
android
程序员江同学15 小时前
Kotlin 技术月报 | 2025 年 1 月
android·kotlin
爱踢球的程序员-116 小时前
Android:View的滑动
android·kotlin·android studio