问题现象
早上业务那边反馈:正式环境管理系统里,点开"供应商证照列表",页面转一圈直接报错白屏。
我本地复现不了,切到正式服看日志,满屏都是 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 时直接炸的。所以问题拆成两块:
- 这 70 行零日期是谁写的?
- 为什么
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 时,也看这四条:
- 是不是用了
*BatchAllColumn或手写全列INSERT IGNORE?这种写法会把 null 打进 SQL,DEFAULT不会生效。 - 时间字段会不会以 null 进入 SQL?写进语句的
NULL不是「没传值」,库不会补默认值。 - 实体或 Service 有没有在插入前赋值?建表语句有默认值,挡不住全字段插入。
- 读路径是不是用
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`。挂在出问题的字段上,不要全局注册。