MyBatis布尔字段映射陷阱全过程解析
在开发过程中,我们常常会遇到一些看似简单却令人困惑的问题。本文记录了一次将 boolean 改为 Boolean 后,MyBatis 插入数据时出现的意外情况。本文不仅逐步揭示了问题的根本原因,还提供了解决方案,并强调了在开发中遵循规范和仔细排查问题的重要性。
为了实现某个功能,需要为已有的表新增字段,其中有一个字段需要表达的含义是:是否有对话条数。
加字段要遵守规范,咱就去看了《阿里巴巴开发规约》的“MySQL规约”,有这么一段描述:
因此,“是否有对话条数”的字段名为 is_has_messages,数据类型为:unsigned tinyint(1表示是,0表示否;默认为1)
给mysql加好字段了,咱还得给 xxxDO 加上字段,按照上面的说法“POJO类中的任何布尔类型的变量,都不要加is前缀”,那就这么写:
/*** 是否有对话条数;1表示是,0表示否 <br>* 默认为1*/private boolean hasMessages;
一切看起来是那么的自然~
![]()
翻车
▐ 奇怪的结果
is_has_messages 咋是0了?true 不应该映射为1吗?
各种检查代码,都没找到原因... 因为相关业务逻辑太简单了,1 + 1 = 2,有啥需要怀疑的吗?
▐ 打印sql
-
拦截器
/*** @author hanxu* @since 2024/12/4*/4j({(type = StatementHandler.class, method = "prepare", args = {Connection.class, Integer.class}),})public class MyBatisSqlInterceptor implements Interceptor {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();}public Object plugin(Object target) {return Plugin.wrap(target, this);}public void setProperties(Properties properties) {// 如果需要可以读取配置}}
<!-- mybatis的配置文件 -->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>
<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打印出来,也没发现啥异常的地方:
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 INTOxxx_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(),?,?,?,?,?,?,?,?,?,?)
![]()
启发
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方法
.../*** @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;}public Object getParameterObject() {return parameterObject;}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 paramsvalue = 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。
private Boolean hasMessages = Boolean.TRUE;
当然了,最后的解法不是注释掉“xxxTalk接口update”的代码,而是:
结语
在这篇文章中,我们经历了一次从困惑到柳暗花明的过程。最终,我们将boolean改为Boolean,并显式地设置了默认值为TRUE,成功解决了问题。这一过程不仅让我们更加了解MyBatis的参数处理机制,也提醒我们在开发中要特别注意类型的选择和默认值的设置,避免因细微的差异而导致意想不到的错误。
相信科学,保持好奇心,勇于探索问题的本质,才能在面对复杂的技术挑战时找到正确的解决方案。希望这篇文章能为各位开发者提供一些有价值的参考,帮助大家在未来的开发中少走弯路。
团队介绍
我们是淘天集团-ideaLAB团队,专注于运用AI工程技术提升集团内部业务的生产效率。我们深入研究PE、RAG、Agent、多模态及模型评测等前沿领域,并积极参与集团内ideaLAB产品的建设。我们的目标是通过开发AI SDK和一站式AI Studio,提供先进的AI应用构建能力。同时,我们致力于将AI工程技术广泛应用于集团的多种业务场景,包括但不限于toC、toB和toP等领域,借助丰富的实践经验,不断探索和实施创新技术,以实现更智能、更高效的业务解决方案。
本文分享自微信公众号 - 大淘宝技术(AlibabaMTT)。
如有侵权,请联系 support@oschina.cn 删除。
本文参与“OSC源创计划”,欢迎正在阅读的你也加入,一起分享。




























