问题现象
早上业务那边反馈:正式环境管理系统里,点开“供应商证照列表”,页面转一圈直接报错白屏。
我本地复现不了,切到正式服看日志,满屏都是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转成 JavaLocalDateTime时直接炸的。所以问题拆成两块:
- 这 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。按环境判断应该是严格模式。之后用 JDBCLocalDateTime去读,默认zeroDateTimeBehavior=EXCEPTION,就会抛零日期异常。外面常包着TransientDataAccessResourceException、LocalDateTimeTypeHandler、DataReadException。
解决方案
知道病根就好动手了,但动手顺序有讲究——先让页面能打开,再堵写入,最后清存量,别一上来就刷数据,入口没堵住,只会导致重复刷数据
第一:先让列表可访问
库里已经躺了 70 行零日期,列表一加载就炸,测试都没法验后面的改动。所以先在cdatetime、mdatetime两个字段上挂自定义TypeHandler,把0000-00-00在读取时转成null。注意是只挂这两个字段,不全局注册,免得波及其它表。
或者说,更简单的方法,可以在SQL返回时,如果判断出是全零的日期,则返回null。Java代码另外判断:
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根本不会触发。所以在调批量插入之前,显式设时间:
LocalDateTimenow=LocalDateTime.now();row.setCdatetime(now);row.setMdatetime(now);两张表都改:供应商证照图片审核子表,以及另一套库里的同构表。
第三步:刷存量
跟产品对了一下,这 70 行是历史脏数据,没有业务含义,统一刷成当前时间。查零日期别用= '0000-00-00',严格模式下不一定查得出来,用CAST(cdatetime AS CHAR) LIKE '0000%'最稳:
SELECTCOUNT(*)FROMyour_tableWHERECAST(cdatetimeASCHAR)LIKE'0000%'ORCAST(mdatetimeASCHAR)LIKE'0000%';避免再次踩坑
这次排障最憋屈的点在于:每一层单独看都没问题,叠在一起就炸。
评审时扫这四件事。Skill 没挂上,或者在改老代码、手写 SQL 时,也看这四条:
- 是不是用了
*BatchAllColumn或手写全列INSERT IGNORE?这种写法会把 null 打进 SQL,DEFAULT不会生效。 - 时间字段会不会以 null 进入 SQL?写进语句的
NULL不是「没传值」,库不会补默认值。 - 实体或 Service 有没有在插入前赋值?建表语句有默认值,挡不住全字段插入。
- 读路径是不是用
LocalDateTime或LocalDate映射datetime?是的话,零日期在驱动层就会抛。
团队用 Cursor 写代码时,把下面几条写进.cursorrules或 Skill。生成时就能拦住,不必等到评审。
## 数据库实体与写入约束 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`。挂在出问题的字段上,不要全局注册。