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技术 | 终端技术 | 音视频技术

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

相关推荐
夏沫琅琊2 小时前
Android 各类日志全面解析(含特点、分析方法、实战案例)
android
程序员JerrySUN3 小时前
OP-TEE + YOLOv8:从“加密权重”到“内存中解密并推理”的完整实战记录
android·java·开发语言·redis·yolo·架构
TeleostNaCl4 小时前
Android | 启用 TextView 跑马灯效果的方法
android·经验分享·android runtime
TheNextByte14 小时前
Android USB文件传输无法使用?5种解决方法
android
quanyechacsdn6 小时前
Android Studio创建库文件用jitpack构建后使用implementation方式引用
android·ide·kotlin·android studio·implementation·android 库文件·使用jitpack
雨中飘荡的记忆6 小时前
MyBatis反射模块详解
java·mybatis
程序员陆业聪6 小时前
聊聊2026年Android开发会是什么样
android
编程大师哥7 小时前
Android分层
android
极客小云8 小时前
【深入理解 Android 中的 build.gradle 文件】
android·安卓·安全架构·安全性测试