MySQL把字段从 TIMESTAMP 改成 DATETIME,线上直接炸了!

事发:一个莫名其妙的空指针

周五下午,我正准备摸鱼等下班,测试同学找过来:库存保存接口报错了,你赶紧看看!

我打开日志一看,好家伙,一片红色:

text 复制代码
java.sql.SQLException: Field 'create_time' doesn't have a default value
at com.mysql.cj.jdbc.exceptions.SQLExceptionsMapping.translateException

嗯?我明明在数据库里给 create_time 设置了 DEFAULT CURRENT_TIMESTAMP,怎么会说没有默认值呢?

而且这段代码上线半年了,一直跑得好好的。仔细一看发版记录,这次只改了一个东西:把 create_time 的字段类型从 TIMESTAMP 改成了 DATETIME。

就这?就改了个字段类型就出问题了?

先看执行的语句,create_time和update_time都设置了默认值,理论上应该不会有问题才对?

sql 复制代码
## MySQL 8.0
ALTER TABLE t_inventory 
MODIFY COLUMN create_time DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
MODIFY COLUMN update_time DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP;

问题复现:诡异的行为差异

先来看看我的实体类:

java 复制代码
@Data
@TableName("t_inventory")
public class Inventory {
    
    @TableField(value = "create_time", fill = FieldFill.INSERT)
    private LocalDateTime createTime;
    
    @TableField(value = "update_time", fill = FieldFill.INSERT_UPDATE)
    private LocalDateTime updateTime;
}

再看看 MyBatis-Plus 打印出来的 SQL:

sql 复制代码
INSERT INTO t_inventory 
( ..., create_time, update_time ) 
VALUES ( ..., ?, ? )

看起来没毛病啊?@TableField(fill = FieldFill.INSERT) 配合数据库 DEFAULT CURRENT_TIMESTAMP,这不是标准用法吗?

但问题就出在这里! 字段有了,值呢? 日志里的参数显示:create_time 的值是 null

这就是报错的直接原因:INSERT 语句显式地插入了 null,而 MySQL 的 DEFAULT 约束只在 INSERT 语句不包含该列时才生效。

深入:代码里埋的"雷"

但这就引发了一个更深的疑问:既然 create_timenull,为什么之前用 TIMESTAMP 的时候没报错?

为了搞清楚这个问题,我做了一个对比实验:

字段类型 INSERT 是否包含该字段 传入的值 MySQL 的行为 结果
TIMESTAMP NULL 自动转换为 CURRENT_TIMESTAMP ✅ 成功
DATETIME NULL 严格模式下拒绝插入 NULL ❌ 报错

TIMESTAMP 会"好心"地把 NULL 转成当前时间,这是 MySQL 的历史行为,算是 TIMESTAMP 类型的一个"隐藏特性"。

而 DATETIME 不会做这种隐式转换,尤其是在 MySQL 8.0 默认开启的严格模式(STRICT_TRANS_TABLES)下,NOT NULL 字段插入 NULL 直接报错。

所以真相是:

  • 以前用 TIMESTAMP:MySQL 默默帮我们把 null 转成了当前时间 → 掩盖了问题
  • 现在用 DATETIME:MySQL 不装了,直接报错 → 问题暴露

但新的问题又来了:为什么 create_time 会是 nullfill = FieldFill.INSERT 不是应该自动填充吗?

带着这个疑问,我又翻了一遍项目代码。

真相:一个经典的"半成品"用法

我搜遍了整个项目,愣是没找到一个 MetaObjectHandler 的实现类。

这就是问题所在:

  • 实体类写了 fill = FieldFill.INSERT → 看起来像是"我要自动填充"
  • 但没人实现 MetaObjectHandler → 实际上根本没人在填充
  • MyBatis-Plus 只能拿到一个 null 值 → 原封不动地塞进了 SQL
  • 以前 TIMESTAMP 帮忙擦了屁股 → 岁月静好
  • 现在 DATETIME 不惯着了 → 直接报错

FieldFill.INSERT 只是一个"声明",真正的填充逻辑在 MetaObjectHandler 里。写了声明却没有实现,这就像是你买了个高级咖啡机,天天按"拿铁"按钮,结果出来的永远是白开水------因为你忘了放咖啡豆啊!

不要问我为什么没实现 MetaObjectHandler,我也不知道,代码来的时候就这样。(反正写这段代码的人已经离职了,这个锅我不背。)

为什么数据库的 DEFAULT 没生效?

这里再补充一个关键知识点:

MySQL 的 DEFAULT 约束只在 INSERT 语句没有显式指定该列时才生效。

sql 复制代码
-- ✅ 没有指定 create_time,DEFAULT 生效
INSERT INTO t_inventory (date, area) VALUES ('2026-09-02', '上海');
-- create_time 自动变成 CURRENT_TIMESTAMP

-- ❌ 指定了 create_time,传入 NULL,DEFAULT 不生效
INSERT INTO t_inventory (date, area, create_time) 
VALUES ('2026-09-02', '上海', NULL);
-- 报错:Column 'create_time' cannot be null

因为 fill = FieldFill.INSERT 的存在,MyBatis-Plus 生成的 INSERT 语句包含了 create_time 列,所以数据库的 DEFAULT CURRENT_TIMESTAMP 根本不会被触发。

解决方案:三条路任选

方案一:去掉 fill 注解(直接快速)

既然没打算实现 MetaObjectHandler,那这个 fill 注解就是掩耳盗铃,不如删掉:

java 复制代码
// 去掉 fill,让数据库的 DEFAULT 来做它该做的事
@TableField("create_time")
private LocalDateTime createTime;

@TableField("update_time")
private LocalDateTime updateTime;

方案二:把 MetaObjectHandler 补上(最规范的)

如果确实需要 Java 层统一管理时间:

java 复制代码
@Component
public class MyMetaObjectHandler implements MetaObjectHandler {
    @Override
    public void insertFill(MetaObject metaObject) {
        this.strictInsertFill(metaObject, "createTime", LocalDateTime.class, LocalDateTime.now());
        this.strictInsertFill(metaObject, "updateTime", LocalDateTime.class, LocalDateTime.now());
    }
    
    @Override
    public void updateFill(MetaObject metaObject) {
        this.strictUpdateFill(metaObject, "updateTime", LocalDateTime.class, LocalDateTime.now());
    }
}

方案三:手动 set 时间(紧急止血)

线上炸了,来不及改代码?先这样顶着:

java 复制代码
entity.setCreateTime(LocalDateTime.now());
entity.setUpdateTime(LocalDateTime.now());

总结:这次踩坑教会我的事

  1. @TableField(fill = FieldFill.INSERT) 只是一个声明,真正的填充逻辑在 MetaObjectHandler 里,两者要配套使用
  2. MySQL 的 DEFAULT 只在 INSERT 不包含该列时才生效,一旦包含了该列,哪怕传的是 null,DEFAULT 也不会触发
  3. TIMESTAMP 会把 NULL 转成当前时间,这个"贴心"的行为可能掩盖了很多问题
  4. DATETIME 在 MySQL 8.0 严格模式下不会容忍 NULL,这其实更符合规范,帮我们暴露了隐藏的问题
  5. 字段类型改动要谨慎,看起来只是改了个类型,背后的行为差异可能超乎想象

最后送大家一句话:代码里写了 fill 就记得实现 MetaObjectHandler,不然就是在给未来的自己埋雷。