LaravelのSchema::enum()はPostgreSQLでCREATE TYPEした型を使わない

  • Laravel
  • PostgreSQL

この記事は、構成と文章の整理にAIを使っています。
あくまで読みやすくするための補助で、内容そのものをAIにすべて任せて書いているわけではありませんが、ところどころ相談しながら整えています。

Laravelのマイグレーションで、PostgreSQLのenum型を定義しようとして以下のようなコードを書いていた。

public function up()
{
DB::statement("CREATE TYPE example_status AS ENUM ('foo', 'bar')");
Schema::table('examples', function (Blueprint $table) {
$table->enum('status', ['foo', 'bar'])->default('foo');
});
}

CREATE TYPEでネイティブのenum型を作った後、$table->enum()でカラムを追加する形。エラーは出ないし一見動いているように見えるが、これは間違っていた。

何が間違っていたか

CREATE TYPE example_status AS ENUM (...)で作ったexample_status型は、このマイグレーションのどこでも参照されていない。$table->enum()は独立した処理で、example_statusという名前とは一切紐付かない。

LaravelのPostgreSQL用グラマーでは、enum()はネイティブのENUM型を使わず、varchar(255)にCHECK制約を付ける実装になっている。

varchar(255) check ("status" in ('foo', 'bar'))

つまり実際にexamplesテーブルに追加されるstatusカラムは、example_status型ではなく単なる制約付きvarcharで、CREATE TYPE文は完全に無駄な処理だった。

なお本来のマイグレーションではenumの値はもっとドメインに紐づいた具体的な文言だったが、ここではfoo/barに匿名化している。

なぜこうなっているらしいか

LaravelはMySQLとPostgreSQLで同じenum()メソッドを共通APIとして提供している都合上、DB間で挙動を揃える必要がある。MySQLはネイティブのENUM型があるが、PostgreSQLのenum()はグラマー層でvarchar+CHECKに変換される作りになっているらしい(実機で生成されたDDLは未確認、Laravel側のグラマー実装のソースで確認)。

副作用として、PostgreSQLのネイティブenumだとALTER TYPE ... ADD VALUEで値を追加できるのに対し、Laravel経由のenumはCHECK制約の張り替えが必要になる。->change()で値を変えようとすると構文エラーになるケースも報告されている。

対処法

方針は2つに分かれる。

PostgreSQLのネイティブENUM型をちゃんと使いたい場合、Laravelの標準enum()は使わず、CREATE TYPEしたあとにaddColumnでカラムを追加する。

DB::statement("CREATE TYPE example_status AS ENUM ('foo', 'bar')");
Schema::table('examples', function (Blueprint $table) {
$table->addColumn('example_status', 'status')->default('foo');
});

Laravel標準のenum()に任せて問題ない場合は、CREATE TYPE自体を書かずvarchar+CHECK制約のまま使う。将来値を変更する頻度が高いなら、こちらの方がCHECK制約の張り替えだけで済む分、運用は楽。

Schema::table('examples', function (Blueprint $table) {
$table->enum('status', ['foo', 'bar'])->default('foo');
});

どちらを選ぶかは、ネイティブENUMの制約変更の面倒さと、CHECK制約のシンプルさのどちらを取るか次第。