全字段 INSERT IGNORE:大约 70 行零日期,把证照列表加载打崩

问题现象

早上业务那边反馈:正式环境管理系统里,点开"供应商证照列表",页面转一圈直接报错白屏。

我本地复现不了,切到正式服看日志,满屏都是 java.sql.SQLException: Zero date value prohibited。这个异常我问了一下元宝------大概就是 JDBC 读到了 0000-00-00 00:00:00 这种非法时间。

于是我赶紧 SELECT 了一下证照表,果然:大概 70 行 cdatetime 全是 0000-00-00 00:00:00。

当时第一反应是"不可能吧?"------这列明明建表时写了 DEFAULT CURRENT_TIMESTAMP,没传值也应该落当前时间才对。

下面就把这批零日期怎么混进去的、以及写入 / 读取 / 存量三处分别怎么补坑,完整记一下。

排查思路

先确认异常来源:Zero date value prohibited 不是业务抛的,是 MySQL 驱动在把 0000-00-00 转成 Java LocalDateTime 时直接炸的。所以问题拆成两块:

  1. 这 70 行零日期是谁写的?
  2. 为什么 DEFAULT CURRENT_TIMESTAMP 没生效?

顺着列表加载的 DAO 反向查写入链路,最后卡在一个自己封装的 insertIgnoreBatch 方法上。它是全字段 INSERT IGNORE,sql_mode 又是严格模式。看到这里基本就有数了------默认值没兜底,null 被静默收成了全零。

根本原因

怎么理解:全字段 INSERT IGNORE 把 Java 的 null 收成了零日期,DEFAULT 没有兜底?

其实是这样的:

insertIgnoreBatchAllColumn 以及任意整列批量插入,会把实体的所有列打进 SQL。Java 侧 LocalDateTime 为 null,SQL 里就是 NULL。业务代码没 set 时间,在这条路径上并不等于走库默认值!!!

DEFAULT 只在 INSERT 不写该列,或该列写成 DEFAULT 关键字时生效。语句里已经是 col = NULL,或写了一个非法日期,默认值不会补上。

INSERT IGNORE 把 NULL 和非法 datetime 收成 0000-00-00 00:00:00。按环境判断应该是严格模式。之后用 JDBC LocalDateTime 去读,默认 zeroDateTimeBehavior=EXCEPTION,就会抛零日期异常。外面常包着 TransientDataAccessResourceException、LocalDateTimeTypeHandler、DataReadException。

解决方案

知道病根就好动手了,但动手顺序有讲究------先让页面能打开,再堵写入,最后清存量,别一上来就刷数据,入口没堵住,只会导致重复刷数据

第一:先让列表可访问

库里已经躺了 70 行零日期,列表一加载就炸,测试都没法验后面的改动。所以先在 cdatetime、mdatetime 两个字段上挂自定义 TypeHandler,把 0000-00-00 在读取时转成 null。注意是只挂这两个字段,不全局注册,免得波及其它表。

或者说,更简单的方法,可以在SQL返回时,如果判断出是全零的日期,则返回null。Java代码另外判断:

sql 复制代码
CASE
  WHEN CAST(cdatetime AS CHAR) LIKE '0000%' THEN NULL
  ELSE cdatetime
END AS cdatetime

用 CAST(col AS CHAR) LIKE '0000%' 是因为严格 SQL 模式下直接拿 datetime = '0000-00-00' 做比较,有时候会被优化器或驱动提前拦掉,CAST 成字符串最稳。

方法有很多,这里我用了SQL的方法;

第二步:堵住写入

insertIgnoreBatch 是全字段插入,Java 侧 LocalDateTime 是 null,SQL 里就是 NULL,DEFAULT CURRENT_TIMESTAMP 根本不会触发。所以在调批量插入之前,显式设时间:

ini 复制代码
LocalDateTime now = LocalDateTime.now();
row.setCdatetime(now);
row.setMdatetime(now);

两张表都改:供应商证照图片审核子表,以及另一套库里的同构表。

第三步:刷存量

跟产品对了一下,这 70 行是历史脏数据,没有业务含义,统一刷成当前时间。查零日期别用 = '0000-00-00',严格模式下不一定查得出来,用 CAST(cdatetime AS CHAR) LIKE '0000%' 最稳:

sql 复制代码
SELECT COUNT(*) FROM your_table
WHERE CAST(cdatetime AS CHAR) LIKE '0000%'
   OR CAST(mdatetime AS CHAR) LIKE '0000%';

避免再次踩坑

这次排障最憋屈的点在于:每一层单独看都没问题,叠在一起就炸。

评审时扫这四件事。Skill 没挂上,或者在改老代码、手写 SQL 时,也看这四条:

  1. 是不是用了 *BatchAllColumn 或手写全列 INSERT IGNORE?这种写法会把 null 打进 SQL,DEFAULT 不会生效。
  2. 时间字段会不会以 null 进入 SQL?写进语句的 NULL 不是「没传值」,库不会补默认值。
  3. 实体或 Service 有没有在插入前赋值?建表语句有默认值,挡不住全字段插入。
  4. 读路径是不是用 LocalDateTime 或 LocalDate 映射 datetime?是的话,零日期在驱动层就会抛。

团队用 Cursor 写代码时,把下面几条写进 .cursorrules 或 Skill。生成时就能拦住,不必等到评审。

markdown 复制代码
## 数据库实体与写入约束

1. 带 `cdatetime`、`mdatetime`、`create_time`、`update_time` 的实体,创建对象时必须显式赋值,禁止把 null 交给 DAO:

    LocalDateTime now = LocalDateTime.now();
    entity.setCdatetime(now);
    entity.setMdatetime(now);

2. 使用全字段 `INSERT IGNORE`(如 `insertIgnoreBatchAllColumn`、手写全列 insert ignore)时,时间字段必须非 null,插入前要生成赋值代码。
3. 读路径用 `LocalDateTime` 或 `LocalDate` 映射 `datetime` 时,要处理零日期 `0000-00-00`。挂在出问题的字段上,不要全局注册。
相关推荐
Json____1 小时前
用 FastAPI + Vue3 打造一套健身私教课预约管理系统
后端·fastapi·it学习·wwwoop.com
aibo8603081 小时前
基于 Spring Boot + Vue3 的博物馆藏品管理系统设计与实现
java·spring boot·后端
苍何1 小时前
Codex+Obsidian,多平台同步,一键分发,太香了!
后端
zzu123zsw1 小时前
把条件校验收敛到枚举接口:Spring Boot 统一断言模式与零污染异常切面工程化
java·spring boot·后端·架构设计
用户93983829415993 小时前
EasyClick苹果群控脚本从零跑通第一个点击:完整教程
后端
陈随易3 小时前
副业搞钱之代办永不过时
前端·后端·程序员
武子康3 小时前
灰度完成率从100%变成50%:五条失败记录怎样消失
人工智能·后端·agent
excel3 小时前
请求很慢,问题一定出在服务器吗?看懂浏览器网络耗时
前端·后端
用户204937554953 小时前
ERes2Net声纹识别短音频匹配不稳定:从音频时长到有效音素的排查实践
后端