在开发过程中,我们常常会遇到一些看似简单却令人困惑的问题。本文记录了一次将 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等领域,借助丰富的实践经验,不断探索和实施创新技术,以实现更智能、更高效的业务解决方案。
¤ 拓展阅读 ¤