引子

最近在项目中碰到了一个问题:项目使用的ORM为JPA,Entity实体中存在下面两条属性:

  • create_time:希望在当前条目添加时,自动设置为当前值。
  • update_time:希望在条目每一次修改时,自动更新为修改时的当前时间。

然而在实际操作中,却出了一些列的问题,在这里,将问题记录并解决。

MySQL版本:5.6.24-log        远程连接工具:Navicat

问题描述 

首先,测试环境数据表的建表语句为:

CREATE TABLE `flag` (
  `id` int(11) NOT NULL AUTO_INCREMENT,
  `content` varchar(200) DEFAULT NULL COMMENT 'Flag内容',
  `create_time` timestamp NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
  `update_time` timestamp NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间',
  PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8;

对应实体如下,其中的lombok注解这里不做解释,请自行百度或者谷歌~

import lombok.*;
import javax.persistence.*;
import java.util.Date;

/**
 * @author Zereao
 * @version 2019/04/10 13:43
 */
@Data
@Entity
@Builder
@NoArgsConstructor
@AllArgsConstructor
@Table(name = "flag")
public class Flag {
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;
    private String content;
    private Date createTime;
    private Date updateTime;
}

对应的DAO层接口定义为:

import org.springframework.data.repository.CrudRepository;
import org.springframework.stereotype.Repository;

/**
 * @author Zereao
 * @version 2019/04/10 13:42
 */
@Repository
public interface FlagDAO extends CrudRepository<Flag, Long> { }

问题一:测试环境,我们在开发的时候,当需要新增一条记录时,如果不手动设置createTime、updateTime字段的值,数据库中对应这两字段的值就是NULL。

插入逻辑:

public void add() {
    Flag flag = Flag.builder().content("测试内容一").build();
    flagDAO.save(flag);
}

插入结果:

MySQL系统变量explicit_defaults_for_timestamp与SQL Mode,MySQL中诡异的Timestamp自动更新

如上所示,数据库中create_time、update_time的值都是NULL,也就是说,我们建表语句中的“DEFAULT CURRENT_TIMESTAMP”并没有起作用!

这时候,我们的解决方式是:首先删除之前的那条create_time/update_time为NULL的记录,否则会报错:【1138 - Invalid use of NULL value】。然后,Navicat选中flag表,菜单选择 设计表,我们将create_time、update_time两个字段选中【不是null】,然后点击保存,如下图:

MySQL系统变量explicit_defaults_for_timestamp与SQL Mode,MySQL中诡异的Timestamp自动更新这时候,我们再执行一遍刚刚的添加接口,结果正常,如下图:【到这里,还是有坑!请看完下文!!!】

MySQL系统变量explicit_defaults_for_timestamp与SQL Mode,MySQL中诡异的Timestamp自动更新

如上图,这里我们添加的时候,代码还是执行的上面的add()方法,并未手动指定两个字段的值,结果数据库自动帮我们设置上了。到这里,我们以为问题解决,于是准备上线。

线上生产环境建表语句,我们加上了两字段不允许为null的限制

CREATE TABLE `flag` (
  `id` int(11) NOT NULL AUTO_INCREMENT,
  `content` varchar(200) DEFAULT NULL COMMENT 'Flag内容',
  `create_time` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
  `update_time` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间',
  PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8;

问题二:测试环境能自动更新时间,线上生产环境却不能自动更新。

就在我们以为问题已经解决了的时候,项目上线,结果却报了错,如下所示:

java.sql.SQLIntegrityConstraintViolationException: Column 'create_time' cannot be null

代码仍然是上面的add()方法,未手动指定create_time、update_time,当我们插入数据时,结果却报错了。这就很奇怪了,我们又去测试环境测试,发现测试环境并没有问题。初步断定应该是生产环境和测试环境 数据库的某一项配置不一致导致的问题。首先,我们赶紧修复问题,通过手动设置两个时间,保证项目正常可用。然后重新上线后,我继续查找资料,尝试解决问题。

通过查资料,针对这个时间自动更新这个功能,JPA其实自己封装了一套处理方案,那就是@CreatedDate、@LastModifiedDate两个注解,分别对应创建时间、最近一次的更新时间。具体使用方法也很简单:

  1. 在启动类上添加@EnableJpaAuditing注解,开启JPA自动适配功能。
  2. 在Entity实体类上添加注解@EntityListeners(AuditingEntityListener.class),开启监听。
  3. 在create_time、update_time字段上分别加上@CreatedDate、@LastModifiedDate注解。

这样,JPA就会自动实现这两个字段的值的更新啦!下图是修改后的Entity实体类:

import lombok.*;
import org.springframework.data.annotation.CreatedDate;
import org.springframework.data.annotation.LastModifiedDate;
import org.springframework.data.jpa.domain.support.AuditingEntityListener;
import javax.persistence.*;
import java.util.Date;

/**
 * @author Zereao
 * @version 2019/04/10 13:43
 */
@Data
@Entity
@Builder
@NoArgsConstructor
@AllArgsConstructor
@Table(name = "flag")
@EntityListeners(AuditingEntityListener.class)
public class Flag {
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;
    private String content;
    @CreatedDate
    private Date createTime;
    @LastModifiedDate
    private Date updateTime;
}

实际上,JPA还提供了@CreatedBy@LastModifiedBy注解,用于保存和更新当前操作用户的信息,这个我不展开讲,有兴趣的同学可以自行查找资料。

到这里,这个问题我们是解决了。但是,到底是什么原因导致了同样的代码,生产环境和测试环境跑起来的结果不一样呢?

通过继续查找资料,我们查到了这篇文章-MySQL timestamp NOT NULL插入NULL的问题,文中提到了【explicit_defaults_for_timestamp】这个MySQL系统变量。通过查阅官方文档,我们又查到了另一个与这个问题有关的概念——【STRICT_TRANS_TABLES】这种【SQL_MODE】。

我们通过通过下面两条语句查看测试环境、生产环境的配置,发现生产环境和测试环境的配置的确不同!

SHOW VARIABLES LIKE '%explicit_defaults_for_timestamp%';

SHOW VARIABLES LIKE '%sql_mode%';

测试环境结果为:

Variable_name Value
explicit_defaults_for_timestamp OFF
sql_mode NO_ENGINE_SUBSTITUTION

生产环境结果为:

Variable_name Value
explicit_defaults_for_timestamp ON
sql_mode STRICT_TRANS_TABLES,NO_ENGINE_SUBSTITUTION

下面,我们展开讲一下这两几概念。

SQL_MODE

关于SQL_MODE,具体可以可以参考官方文档。官方文档中提到了这么一句话:

Modes affect the SQL syntax MySQL supports and the data validation checks it performs.

简而言之, SQL_MODE会影响MySQL支持的SQL语法以及它执行的数据验证检查。这里不多讲,与我们这个问题有关的SQL_MODE是【STRICT_TRANS_TABLES】这种模式,下面接着讲。

STRICT_TRANS_TABLES

当我们启用了STRICT_TRANS_TABLES后,表示启用了“严格的SQL模式”中的一种,另一种严格SQL模式是:“ STRICT_ALL_TABLES”。官方文档中是这么描述的:

Strict mode controls how MySQL handles invalid or missing values in data-change statements such as INSERT or UPDATE. 

If strict mode is not in effect, MySQL inserts adjusted values for invalid or missing values and produces warnings. 

For transactional tables, an error occurs for invalid or missing values in a data-change statement when either STRICT_ALL_TABLES or STRICT_TRANS_TABLES is enabled. The statement is aborted and rolled back.

翻译一下:

严格模式控制MySQL如何处理INSERT或UPDATE等数据更改语句中的无效值或缺失值。

如果严格模式未启用,MySQL会为无效或缺失的值插入调整后的值,并产生警告。

对于事务表,当启用STRICT_ALL_TABLES或 STRICT_TRANS_TABLES启用时,数据更改语句中的值无效或缺失时会发生错误 。该声明被中止并回滚。

PS:

事务性表、非事务性表,可以参考MySQL的事务性表和非事务性表这篇文章。

我们的表一般都是InnoDB引擎,所以是事务性表,在STRICT_TRANS_TABLES启用时,错误值将会报错。

explicit_defaults_for_timestamp

对explicit_defaults_for_timestamp这个系统变量详细的讲解,请参考官方文档。MySQL 8.0的官方文档这么介绍的:

This system variable determines whether the server enables certain nonstandard behaviors for default values and NULL-value handling in TIMESTAMP columns. By default, explicit_defaults_for_timestamp is enabled, which disables the nonstandard behaviors. Disabling explicit_defaults_for_timestamp results in a warning.

翻译一下:

此系统变量确定服务器是否为TIMESTAMP列中的默认值和NULL值处理启用某些非标准行为。默认情况下, explicit_defaults_for_timestamp 是启用状态,同时这将禁用非标准行为。禁用 explicit_defaults_for_timestamp 会导致警告。

针对explicit_defaults_for_timestamp的用法,官方文档是这么介绍的:

If explicit_defaults_for_timestamp is disabled, the server enables the nonstandard behaviors and handles TIMESTAMP columns as follows:

  • TIMESTAMP columns not explicitly declared with the NULL attribute are automatically declared with the NOT NULL attribute. Assigning such a column a value of NULL is permitted and sets the column to the current timestamp.
  • The first TIMESTAMP column in a table, if not explicitly declared with the NULL attribute or an explicit DEFAULT or ON UPDATE attribute, is automatically declared with the DEFAULT CURRENT_TIMESTAMP and ON UPDATE CURRENT_TIMESTAMP attributes.
  • TIMESTAMP columns following the first one, if not explicitly declared with the NULL attribute or an explicit DEFAULT attribute, are automatically declared as DEFAULT '0000-00-00 00:00:00' (the “zero” timestamp). For inserted rows that specify no explicit value for such a column, the column is assigned '0000-00-00 00:00:00' and no warning occurs.
  • Depending on whether strict SQL mode or the NO_ZERO_DATE SQL mode is enabled, a default value of '0000-00-00 00:00:00' may be invalid. Be aware that the TRADITIONAL SQL mode includes strict mode and NO_ZERO_DATE.

If explicit_defaults_for_timestamp is enabled, the server disables the nonstandard behaviors and handles TIMESTAMP columns as follows:

  • It is not possible to assign a TIMESTAMP column a value of NULL to set it to the current timestamp. To assign the current timestamp, set the column to CURRENT_TIMESTAMP or a synonym such as NOW().
  • TIMESTAMP columns not explicitly declared with the NOT NULL attribute are automatically declared with the NULL attribute and permit NULL values. Assigning such a column a value of NULL sets it to NULL, not the current timestamp.
  • TIMESTAMP columns declared with the NOT NULL attribute do not permit NULL values. For inserts that specify NULL for such a column, the result is either an error for a single-row insert or if strict SQL mode is enabled, or '0000-00-00 00:00:00' is inserted for multiple-row inserts with strict SQL mode disabled. In no case does assigning the column a value of NULL set it to the current timestamp.
  • TIMESTAMP columns explicitly declared with the NOT NULL attribute and without an explicit DEFAULT attribute are treated as having no default value. For inserted rows that specify no explicit value for such a column, the result depends on the SQL mode. If strict SQL mode is enabled, an error occurs. If strict SQL mode is not enabled, the column is declared with the implicit default of '0000-00-00 00:00:00' and a warning occurs. This is similar to how MySQL treats other temporal types such as DATETIME.
  • No TIMESTAMP column is automatically declared with the DEFAULT CURRENT_TIMESTAMP or ON UPDATE CURRENT_TIMESTAMP attributes. Those attributes must be explicitly specified.
  • The first TIMESTAMP column in a table is not handled differently from TIMESTAMP columns following the first one.

这里不翻译了,简单归纳下:

如果 explicit_defaults_for_timestamp 禁用

  • 建表的时候,如果TIMESTAMP列没有显式地声明为NULL,则该列将自动声明NOT NULL。为TIMESTAMP列分配值的时候,为此列分配NULL值是允许的,但此时此列的值将被设置为当前时间戳。
  • 建表的时候,表中的第一个TIMESTAMP列,只要满足以下三种情况之一,都将自动使用DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP属性进行声明 。
    1. 没有显式地声明NULL属性(例如,文中最开始测试环境中,`create_time` timestamp NULL DEFAULT CURRENT_TIMESTAMP语句中的NULL属性)
    2. 没有使用DEFAULT显式指定默认值
    3. 没有使用 ON UPDATE 属性
  • 建表的时候,第一个TIMESTAMP列后面的TIMESTAMP列,如果没有使用NULL属性进行显式声明,或者没有使用DEFAULT显式指定默认值,则自动声明为 DEFAULT '0000-00-00 00:00:00'(官方文档中称为【the “zero” timestamp】)。对于未为此类列指定显式值的插入行,该列将被分配值 '0000-00-00 00:00:00' ,并且不会发出警告。针对这一点,如果启用了“严格SQL模式”(比如,上文提到的STRICT_TRANS_TABLES模式)或者“NO_ZERO_DATE”模式,这里的零值—— '0000-00-00 00:00:00' 可能会失效。

如果 explicit_defaults_for_timestamp 启用

  • 设置值的时候,不能通过为TIMESTAMP列分配NULL值来将其设置为当前时间戳。如果要设置当前时间戳,需要在SQL语句中手动设置值为 CURRENT_TIMESTAMP ,或者其他意思相同的替代,例如NOW();
  • 建表的时候,如果TIMESTAMP列未显式指定为NOT NULL,那么它将自动使用NULL属性进行声明,并且允许被设置为NULL值。如果为此列设置一个NULL值,那么此列的值将会被设置为 NULL,而不是当前时间戳。
  • 设置值的时候,使用NOT NULL属性声明的TIMESTAMP列不允许被设置为NULL值。对于INSERT操作,为此类列指定插入NULL值,结果要么是单行插入错误或启用了严格的SQL模式,或者在禁用严格SQL模式的情况下插入多行'0000-00-00 00:00:00'。在任何情况下,都不会将该列的NULL值分配给当前时间戳。水平有限,感觉翻译的不太准确,自己理解的也不够透彻,仅供参考。
  • 使用该NOT NULL属性显式声明,但没有显式指定DEFAULT属性的TIMESTAMP列被视为没有默认值。对于为此类列没有明确指定值的插入行,结果取决于SQL模式。如果启用了严格的SQL模式,则会发生错误。如果未启用严格SQL模式,则使用隐式默认值 '0000-00-00 00:00:00' 声明该列,并发出警告。这与MySQL处理其他时间类型的方式相同,如 DATETIME。
  • TIMESTAMP列不会自动声明为 DEFAULT CURRENT_TIMESTAMP 或者 ON UPDATE CURRENT_TIMESTAMP,必须明确指定这些属性。
  • 表中第一个TIMESTAMP列的处理方式与第一个TIMESTAMP列之后的TIMESTAMP列没有区别。

案例:

如果explicit_defaults_for_timestamp被禁用,并且没有启用严格模式,在我们测试环境重新建立一张表,建表语句如下:

CREATE TABLE `text` (
  `id` int(11) NOT NULL AUTO_INCREMENT,
  `content` varchar(200) DEFAULT NULL COMMENT '内容',
  `create_time` timestamp COMMENT '创建时间',
  `update_time` timestamp COMMENT '更新时间',
  PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8;

这里,建表语句中有两个TIMESTAMP列,并且都没有显式使用NULL属性,也没有使用DEFAULT和ON UPDATE属性。建表成功后,我们查看所建立表的DDL语句:

CREATE TABLE `text` (
  `id` int(11) NOT NULL AUTO_INCREMENT,
  `content` varchar(200) DEFAULT NULL COMMENT '内容',
  `create_time` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '创建时间',
  `update_time` timestamp NOT NULL DEFAULT '0000-00-00 00:00:00' COMMENT '更新时间',
  PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8;

我们可以看到,完全符合上面官方文档中所提的explicit_defaults_for_timestamp 禁用的情况。

总结:

在本次上线出现的问题中,原因是:

在测试环境,explicit_defaults_for_timestamp 禁用。在我们修改create_time、update_time两个字段的属性为 NOT NULL 后,我们的create_time、update_time这两个字段可以仍然支持null值插入,只不过对应的TIMESTAMP列将会被自动设置为当前时间戳。所以,在测试环境,当我们新添加一条记录时,即使都为NULL,create_time和update_time两个字段总是会自动设置为当前时间戳。

而在生产环境,explicit_defaults_for_timestamp 启用。JPA在执行Save()方法的时候,Flag实体的createTime、updateTime的属性都还为null,对于create_time、update_time两个字段,我们明确指定了NOT NULL,所以,这两字段不允许被设置为NULL值,同时也不能通过为两字段分配NULL来设置当前时间戳,所以会报错。

而对于严格SQL模式,我理解的是,它影响的是零值,而不是NULL值。例如:

-- 1、设置当前Session的explicit_defaults_for_timestamp为禁用;同时,严格SQL模式是启用的
SET @@SESSION.explicit_defaults_for_timestamp = 'OFF';
-- 2、语句一,测试此时能否插入 NULL 值
INSERT INTO `flag` (`content`, `create_time`, `update_time`) VALUES ('测试1', null, null);
-- 3、语句二,测试 零值 是否正常插入
INSERT INTO `flag` (`content`, `create_time`, `update_time`) VALUES ('测试2', null, '0000-00-00 00:00:00');

首先,我们禁用当前Session的explicit_defaults_for_timestamp,然后分别执行两条语句。结果上面的 语句一正常执行,并且create_time、update_time两个字段都被更新为当前时间戳;而语句三则报错,如下:

1292 - Incorrect datetime value: '0000-00-00 00:00:00' for column 'update_time' at row 1, Time: 0.021000s

综上,总结下来还是,JPA用的不熟练,不知道JPA本身已经提供了@CreatedBy@LastModifiedBy注解来支持时间戳自动更新。

最后,博主能力有限,也还在不断学习,如果有什么地方描述的不准确,或者有什么地方没讲明白,欢迎大家留言交流,希望和大家一起努力~

参考文献:

1、https://dev.mysql.com/doc/refman/8.0/en/server-system-variables.html#sysvar_explicit_defaults_for_timestamp

2、https://www.jianshu.com/p/30aef87f3171

3、https://blog.csdn.net/tianyaleixiaowu/article/details/77931903

4、https://www.cnblogs.com/suifu/p/5823052.html

5、http://seanlook.com/2016/04/22/mysql-sql-mode-troubleshooting/

6、https://blog.csdn.net/shaochenshuo/article/details/50577868

7、https://dev.mysql.com/doc/refman/8.0/en/sql-mode.html

8、https://www.cnblogs.com/52haiyan/p/9546027.html

相关文章: