1. 问题背景与现象还原
最近在开发一个金融报表系统时,遇到了一个诡异的MySQL报错问题。我们的技术栈是Spring Boot + MyBatisPlus + MySQL 8.0,在批量插入报表数据时,偶尔会出现Field 'end_time1' doesn't have a default value的错误。这个报错看似普通,但实际排查过程却相当曲折。
先还原一下问题场景:
- 数据库表结构设计中,
end_time1和end_time2字段都明确设置了DEFAULT NULL - 使用MyBatisPlus的
save()方法插入数据时,这两个字段没有赋值 - 生成的SQL语句确实没有包含这两个字段
- 但奇怪的是,这个错误是偶发的,直接执行生成的SQL语句却能成功
2. 初步排查与困惑点
2.1 常规排查路径
遇到这种报错,我们通常会按照以下步骤排查:
- 检查表结构:确认字段是否真的允许NULL
sql复制SHOW CREATE TABLE aaaa;
确认end_time1和end_time2字段确实有DEFAULT NULL的设置
- 检查SQL模式:查看MySQL的SQL模式设置
sql复制SELECT @@GLOBAL.sql_mode, @@SESSION.sql_mode;
- 检查MyBatisPlus配置:确认是否有全局的字段处理策略
2.2 发现的矛盾点
排查后发现几个矛盾现象:
- 表结构确实允许NULL值
- SQL模式是默认的
ONLY_FULL_GROUP_BY,STRICT_TRANS_TABLES,NO_ZERO_IN_DATE,NO_ZERO_DATE,ERROR_FOR_DIVISION_BY_ZERO,NO_ENGINE_SUBSTITUTION - 同样的SQL语句,有时能执行成功,有时会报错
- 直接在MySQL客户端执行生成的SQL语句却总是成功
3. 深入分析与根本原因
3.1 MySQL的严格模式陷阱
经过深入排查,发现问题出在MySQL的STRICT_TRANS_TABLES模式上。虽然表结构允许NULL,但在严格模式下,MySQL对NULL值的处理有一些特殊规则:
- 当STRICT_TRANS_TABLES启用时,如果INSERT语句没有显式指定允许NULL的列,且没有DEFAULT值,MySQL会报错
- 这个行为在批量插入时尤为明显,因为MySQL会尝试优化批量插入的执行计划
3.2 MyBatisPlus的字段处理机制
MyBatisPlus的save()方法默认会忽略NULL值的字段,不会将它们包含在INSERT语句中。这与MySQL严格模式的要求产生了冲突:
- 表设计:字段允许NULL,有DEFAULT NULL
- MyBatisPlus:不包含NULL字段
- MySQL严格模式:要求显式指定NULL或DEFAULT
3.3 偶现问题的原因
这个错误之所以偶现,是因为:
- 当批量插入的记录数较少时,MySQL可能不会启用特定的优化策略
- 当记录数达到某个阈值时,MySQL会改变执行计划,触发严格模式的检查
- 直接执行SQL能成功是因为客户端通常不会启用严格模式
4. 解决方案与实施细节
4.1 方案一:显式指定NULL值
java复制// 在实体类中显式初始化字段为null
public class Report {
private LocalDateTime endTime1 = null;
private LocalDateTime endTime2 = null;
// 其他字段...
}
4.2 方案二:修改MyBatisPlus配置
yaml复制mybatis-plus:
global-config:
db-config:
insert-strategy: not_empty # 只忽略空字符串,不忽略null
4.3 方案三:调整MySQL SQL模式
sql复制SET SESSION sql_mode = 'ONLY_FULL_GROUP_BY,NO_ZERO_IN_DATE,NO_ZERO_DATE,ERROR_FOR_DIVISION_BY_ZERO,NO_ENGINE_SUBSTITUTION';
4.4 最终采用的解决方案
我们选择了方案一,因为:
- 最符合业务语义 - 这些字段确实可能为NULL
- 不影响其他场景的SQL生成
- 不需要修改数据库配置,便于部署
5. 深入原理与最佳实践
5.1 MySQL严格模式详解
STRICT_TRANS_TABLES模式下,MySQL会对事务表执行严格的校验:
- 检查所有没有默认值的非NULL字段是否被显式赋值
- 对NULL值的处理更加严格
- 影响INSERT、UPDATE等操作
5.2 MyBatisPlus字段策略对比
MyBatisPlus提供了几种字段插入策略:
not_null:只包含非NULL字段(默认)not_empty:包含非NULL且非空字符串字段ignore:包含所有字段never:不包含主键外的任何字段
5.3 数据库设计建议
-
对于可能为NULL的字段,建议:
- 显式设置DEFAULT NULL
- 在应用层明确处理NULL值
-
对于不允许NULL的字段:
- 设置合理的默认值
- 确保应用层总是赋值
6. 扩展思考与相关问题
6.1 批量插入的性能考量
当使用MyBatisPlus进行批量插入时,还需要注意:
java复制// 正确的批量插入方式
List<Report> reports = ...;
reportService.saveBatch(reports, 1000); // 分批插入
6.2 其他ORM框架的对比
- JPA/Hibernate:默认会包含NULL字段
- JDBC Template:完全由开发者控制
- MyBatis:需要手动编写SQL或使用动态SQL
6.3 监控与预警机制
建议对这类问题建立监控:
- 监控MySQL错误日志中的严格模式错误
- 在应用层捕获并记录SQL异常
- 对新表设计进行严格评审
7. 经验总结与避坑指南
经过这次排查,我总结了以下几点经验:
-
不要忽视"简单"的错误:看似简单的报错可能隐藏着框架和数据库的复杂交互
-
理解各层的默认行为:
- MySQL的严格模式
- ORM框架的字段处理策略
- 连接池的配置
-
建立完整的测试用例:
- 单条插入测试
- 批量插入测试
- 边界值测试
-
文档化数据库约定:
- NULL字段的处理规范
- 默认值的设置原则
- SQL模式的统一配置
这个问题的解决过程让我深刻体会到,在数据库应用开发中,理解每一层的默认行为和它们之间的交互是多么重要。希望这个案例能帮助其他开发者避免类似的坑。