最近在开发一个金融报表系统时,遇到了一个诡异的MySQL报错。系统使用MySQL 8作为数据库,ORM框架选用的是MyBatisPlus。在某个报表数据插入场景中,出现了Field 'end_time1' doesn't have a default value的错误,但奇怪的是:
end_time1和end_time2字段明明设置了DEFAULT NULL作为有多年MySQL调优经验的DBA,这种"薛定谔的报错"引起了我的高度警觉。经过深入排查,发现这是一个典型的"ORM特性+MySQL严格模式"组合拳造成的问题。
首先确认表结构定义:
sql复制CREATE TABLE `aaaa` (
`id` bigint NOT NULL AUTO_INCREMENT,
`serial_no` int DEFAULT NULL,
`business_date` int DEFAULT NULL,
`market_no` int DEFAULT NULL,
`report_code` varchar(20) DEFAULT NULL,
`end_time1` datetime DEFAULT NULL, -- 明确设置了DEFAULT NULL
`end_time2` datetime DEFAULT NULL,
PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
确认这两个datetime字段确实允许NULL且有默认值NULL,理论上不应该出现这个报错。
通过SHOW VARIABLES LIKE 'sql_mode'查看MySQL服务器配置,发现了关键线索:
code复制STRICT_TRANS_TABLES,NO_ENGINE_SUBSTITUTION
STRICT_TRANS_TABLES是严格模式的一种,在这种模式下:
注意:这与我们常规认知的"可空字段会自动用NULL填充"不同,是MySQL严格模式下的特殊行为
MyBatisPlus的save()方法有个默认行为:
示例代码:
java复制@Data
public class Report {
private Long id;
private Integer serialNo;
private Integer businessDate;
private Integer marketNo;
private String reportCode;
private Date endTime1; // 值为null时会被MP忽略
private Date endTime2;
}
// 调用方式
reportMapper.save(report);
如原文所述,可以通过在实体类中显式设置null值:
java复制report.setEndTime1(null);
report.setEndTime2(null);
这样MyBatisPlus就会在SQL中显式插入NULL,规避严格模式检查。
修改MySQL配置,移除STRICT_TRANS_TABLES:
sql复制SET GLOBAL sql_mode='NO_ENGINE_SUBSTITUTION';
或者在my.cnf中配置:
code复制[mysqld]
sql_mode=NO_ENGINE_SUBSTITUTION
这样MySQL会回退到宽松模式,对省略的可空字段自动填充NULL。
在application.yml中配置:
yaml复制mybatis-plus:
global-config:
db-config:
insert-strategy: not_empty # 原默认值是not_null
这个配置会让MyBatisPlus:
在STRICT_TRANS_TABLES模式下,字段处理流程如下:
code复制检查INSERT语句 →
字段是否显式指定值?
→ 是:使用指定值
→ 否:检查是否有DEFAULT定义
→ 有:使用DEFAULT值
→ 无:报错"doesn't have a default value"
关键点:字段在SQL中"未出现"和"显式设为NULL"是两种不同的语义。
默认的not_null策略下:
java复制if (value != null) {
// 包含字段
} else {
// 完全省略字段
}
改为not_empty后:
java复制if (value != null || insertStrategy == NOT_EMPTY) {
// 包含字段,值为NULL
}
严格模式的优点:
缺点:
对于新项目建议:
yaml复制mybatis-plus:
global-config:
db-config:
insert-strategy: not_empty
update-strategy: not_empty
java复制@TableField(insertStrategy = FieldStrategy.IGNORED)
private Date endTime1;
建议对这类错误添加监控:
sql复制-- 在应用日志中监控错误代码
ERROR 1364 (HY000): Field 'xxx' doesn't have a default value
-- 或使用performance_schema
SELECT * FROM performance_schema.events_errors_summary_global_by_error
WHERE ERROR_NAME LIKE '%HAS_NO_DEFAULT%';
默认会生成完整的INSERT语句,对null值字段显式插入NULL,因此不会出现此问题。
需要开发者在XML中明确每个字段的处理逻辑,灵活性更高但更繁琐:
xml复制<insert id="insert">
INSERT INTO aaaa
<trim prefix="(" suffix=")" suffixOverrides=",">
serial_no,
<if test="endTime1 != null">end_time1,</if>
</trim>
VALUES
<trim prefix="(" suffix=")" suffixOverrides=",">
#{serialNo},
<if test="endTime1 != null">#{endTime1},</if>
</trim>
</insert>
这个问题在不同版本的表现:
遇到类似"doesn't have a default value"报错时,可以按照以下步骤排查:
确认表结构定义
sql复制SHOW CREATE TABLE table_name;
检查当前SQL模式
sql复制SELECT @@sql_mode;
分析实际执行的SQL
确认ORM框架的字段处理策略
检查是否有触发器、约束等附加逻辑
不同解决方案的性能对比:
| 方案 | 优点 | 缺点 |
|---|---|---|
| 严格模式+显式NULL | 数据一致性高 | 需要修改代码 |
| 关闭严格模式 | 兼容性好 | 可能掩盖数据问题 |
| 修改MP配置 | 一劳永逸 | 影响所有null字段处理逻辑 |
在百万级插入测试中,三种方案的性能差异在3%以内,建议以业务需求为选择标准。
经过这个案例,我在团队中建立了新的MySQL开发规范:
yaml复制db-config:
insert-strategy: not_empty
update-strategy: not_empty
java复制@TableField(insertStrategy = FieldStrategy.NOT_EMPTY)
private Date endTime;
对于已经上线的系统,建议分阶段改造: