您现在的位置是:首页 > 文章详情

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

日期:2024-12-13点击:124



在开发过程中,我们常常会遇到一些看似简单却令人困惑的问题。本文记录了一次将 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  * @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 version="1.0" encoding="UTF-8" ?><!-- mybatis的配置文件 --><!DOCTYPE configurationPUBLIC "-//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 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打印出来,也没发现啥异常的地方:

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(), ?, ?, ?, ?, ?, ?, ?, ?, ?, ? )


启发


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; }
@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。

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等领域,借助丰富的实践经验,不断探索和实施创新技术,以实现更智能、更高效的业务解决方案。



¤  拓展阅读  ¤

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

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





本文分享自微信公众号 - 大淘宝技术(AlibabaMTT)。
如有侵权,请联系 support@oschina.cn 删除。
本文参与“OSC源创计划”,欢迎正在阅读的你也加入,一起分享。

原文链接:https://my.oschina.net/u/4662964/blog/16734949
关注公众号

低调大师中文资讯倾力打造互联网数据资讯、行业资源、电子商务、移动互联网、网络营销平台。

持续更新报道IT业界、互联网、市场资讯、驱动更新,是最及时权威的产业资讯及硬件资讯报道平台。

转载内容版权归作者及来源网站所有,本站原创内容转载请注明来源。

文章评论

共有0条评论来说两句吧...

文章二维码

扫描即可查看该文章

点击排行

推荐阅读

最新文章