1. 前言

最近在完善仓库管理系统,入库有一个预计到货时间(planned_at)选项,然后有一个入库完成时间(finished_at)选项,但在更新入库完成时间的时候,预计到货时间一直在更新。一开始以为是 ORM 自动更新时间戳或者自动类型转换的锅,后来发现是 MySQL 的 TIMESTAMP 类型造成的。下面详细分析一波。

2. 问题说明

数据库迁移文件:

.
.
.
    public function up()
    {
        Schema::create('foo', function (Blueprint $table) {
            $table->increments('id');
            $table->timestamp('planned_at');
            $table->timestamp('finished_at')->nullable();
            $table->timestamps();
        });
    }
.
.
.

模型文件:

app/Models/Foo.php

.
.
.
    protected $guarded = [];

    protected $dates = [
        'planned_at',
        'finished_at'
    ]; 
.
.
.

在更新完成时间时:

.
.
.
$foo = Foo::where('id', 1)->first();
$foo->finished_at = now();
$foo->save();
.
.
.

很奇怪的是 planned_at 也被修改了,但是我们并没有更新该属性。

3. 原因分析

问题就出在 MySQL 的 TIMESTMAP 类型上 。我们查看下实际生成的 SQL 语句:

CREATE TABLE `foos` (
  `id` int(10) unsigned NOT NULL AUTO_INCREMENT,
  `planned_at` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
  `finished_at` timestamp NULL DEFAULT NULL,
  `created_at` timestamp NULL DEFAULT NULL,
  `updated_at` timestamp NULL DEFAULT NULL,
  PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;

planned_at 默认值为当前时间戳,在对该记录执行更新操作时默认值也为当前时间戳。而 finished_at 默认为 NULL,不会出现该问题,这就是导致问题的原因。

TIMESTAMP 的变体

  1. TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP
    在创建新记录和修改现有记录的时候都对这个数据列更新为当前时间戳。
  2. TIMESTAMP DEFAULT CURRENT_TIMESTAMP
    在创建新记录的时候把这个字段设置为当前时间,但以后修改时,不再更新它。
  3. TIMESTAMP ON UPDATE CURRENT_TIMESTAMP
    在创建新记录的时候把这个字段设置为 0,以后修改时刷新它。
  4. TIMESTAMP DEFAULT ‘yyyy-mm-dd hh:mm:ss’ ON UPDATE CURRENT_TIMESTAMP
    在创建新记录的时候把这个字段设置为给定值,以后修改时刷新它。

4. 总结

在 Laravel 的数据库迁移文件中定义 不可为空 的 TIMESTAMP 时,尤其注意 TIMESTAMP 会自动更新的问题。建议定义 可为空 的 TIMESTAMP,然后在验证中设置验证规则为必填项。