発生した問題
Laravelのマイグレーションで作成したTIMESTAMP型カラムが、明示的に値を指定していないUPDATE実行時に自動更新されるという事象に遭遇しました。原因を調査したので、その内容を共有します。
環境
- PHP(Laravel)
- MariaDB 10.10未満
再現コード
以下のマイグレーションでカラムを作成します。
Schema::create('your_table', function (Blueprint $table) {
$table->id();
$table->timestamp('token_expires');
});
このテーブルに対してレコードをUPDATEすると、token_expiresを指定していないにもかかわらず値が更新されます。
原因
MariaDBの自動付与仕様
MariaDB 10.10未満では、システム変数explicit_defaults_for_timestampが未設定の場合(これがデフォルト値)、テーブル内で最初に定義されたTIMESTAMP型カラムに対し、NULL属性、DEFAULT句、ON UPDATE句のいずれも明示していなければ、次の属性が自動的に付与されます。
DEFAULT current_timestamp()ON UPDATE current_timestamp()
この仕様には、押さえておくべきポイントが2つあります。
- 対象になるのは最初のTIMESTAMP型カラムのみです。2番目以降のTIMESTAMP型カラムは、DEFAULT句を省略していても自動付与の対象にはなりません。
- 「肩代わり」は発生しません。先頭のTIMESTAMP型カラムが何らかの理由(NULL属性の明示など)で対象外だった場合でも、その特別扱いが次のカラムに移ることはありません。
今回のケースでは、token_expiresがテーブル内で最初に定義されたTIMESTAMP型カラムだったため、この自動付与の対象になったと考えられます。
なお、$table->timestamps()でcreated_at/updated_atを先に定義していれば、話は変わります。Laravelはこれらをnullable()付き、つまりNULL属性を明示した状態で生成するため、自動付与の対象から除外され、後続のカラムにも影響しません。
Laravel側の挙動
$table->timestamp('token_expires')と書いた場合、Laravelはnullable()を指定しない限りNOT NULLを付与しますが、DEFAULT句については何も出力しません。
結果として、MariaDBに送られるSQLは次のようになります。
CREATE TABLE `your_table` (
`id` BIGINT UNSIGNED NOT NULL AUTO_INCREMENT,
`token_expires` TIMESTAMP NOT NULL,
PRIMARY KEY (`id`)
);
NOT NULLは指定されていても、DEFAULT句、ON UPDATE句が明示されていないため、token_expiresが最初のTIMESTAMP型カラムであるこのテーブルでは、MariaDB(10.10未満)側がDEFAULT current_timestamp() ON UPDATE current_timestamp()を自動で埋め込んでしまいます。
まとめ
- MariaDB 10.10未満は、
explicit_defaults_for_timestampが未設定だと、テーブル内最初のTIMESTAMP型カラムにDEFAULT/ON UPDATE句を自動付与する仕様を持っています。 - 対象になるのは先頭のTIMESTAMP型カラムのみで、対象外になった分が後続カラムに引き継がれることはありません。
- Laravelの
$table->timestamp()はDEFAULT句を出力しないため、意図せずこの自動付与仕様の対象になり得ます。 $table->timestamps()でcreated_at/updated_atを先に定義しておくと、これらがNULL許容で生成されるため、後続のTIMESTAMP型カラムを自動付与仕様から守ることができます。