【Laravel】enumを使える場合、使ってはいけない場合

# はじめに
この記事ではタイトル通り、Laravelでenumを使える場合、使えない（非推奨の）場合を説明していきます。
本題に入る前に筆者の紹介を軽くすると、筆者はWeb系の企業で働いているインターン生です。実務でenum型を使う機会があったのですが、Laravelで扱うときに若干のクセがあり、そのクセをまとめようと考えました。
Laravelでenumを使うか迷っている方の参考になれば幸いです。
本題の部分だけ見たい方は[こちら](#enumを使える場合)

# enumとは何か
enumを使おうと思っているけどあまり詳しく知らないという方もいるかと思います。enumとはSQLのデータ型の一つで、挿入するデータを制限することができます。
詳しく説明されている記事があったので[こちら](https://qiita.com/igm50/items/8c9788d4ba5868642c69)を読んでからこの記事を読むことをお勧めします。

https://qiita.com/igm50/items/8c9788d4ba5868642c69

# enumのメリット
enumを使う一番のメリットはデータの取得・更新などが楽なことです。
通常マスターテーブルのようなものが必要な実装も、enumを使うことでテーブル数を削減できます。
JOINが不要になる分、パフォーマンスも向上します。
実際に例を見ながら考えます。まずは通常のenumを使わないパターンから見ていきます。

<font color="Green">usersテーブル</font>

|id|name|ability_id|
|:-:|:-:|:-:|
|1|竈門炭治郎|2|
|2|煉獄杏寿郎|1|
|3|我妻善逸|3|
|4|冨岡義勇|2|

<font color="Green">abilitiesテーブル</font>

|id|name|
|:-:|:-:|
|1|炎の呼吸|
|2|水の呼吸|
|3|雷の呼吸|


上記のテーブルはenumを使わない場合のテーブル設計です。enum型を使わずにリレーションを張ってデータを管理する場合、通常このように2つ以上テーブルが必要になります。
ユーザーのabilityを取得したいという場合は、Modelにリレーションの記述をし`$user->ability->name`のように取得する、もしくは`leftJoin()`などを使う必要があります。データが複雑になってくると、記述量も増え、頭を使わないとクエリを書くのが難しくなります。
それではenumを使った場合のテーブルも見てみます。

|id|name|ability(`enum('炎の呼吸', '水の呼吸', '雷の呼吸')`)|
|:-:|:-:|:-:|
|1|竈門炭治郎|水の呼吸|
|2|煉獄杏寿郎|炎の呼吸|
|3|我妻善逸|雷の呼吸|
|4|冨岡義勇|水の呼吸|

enumを使う場合はテーブルの数が減ります。今回の場合は先ほどあったabilitiesテーブルが不要になります。
また、データの取得も先ほどよりシンプルな記述になります。ユーザーのabilityを取得したい場合は`$user->ability`で済みますし、もちろん`leftJoin()`する必要もありません。

楽に記述できるのは十分なメリットだと思います。DBの設計もシンプルになり、開発もしやすくなるのではないかと思います。

# enumのデメリット
ここからがこの記事の本題です。冒頭でも述べさせて頂いたように、このenumというデータ型、良いところだけではありません。<strong>かなり</strong>厄介な部分が多いです。一つずつ紹介していきます。

#### 1. migrationでのカラム変更がしにくい
まず、通常のカラム変更を見てみましょう。

```php:database\migrations\change_user_table.php
public function up()
    {
        Schema::table('users', function (Blueprint $table) {
            $table->bigInteger('ability_id')->default(1)->change(); // ability_idのデフォルトを1に設定
        });
    }
```
これが通常のmigrationでのカラム変更になります。

ではenum型の場合はどうでしょうか。

```php:database\migrations\change_user_table.php
public function up()
    {
        Schema::table('users', function (Blueprint $table) {
            $table->enum('ability')->default('炎の呼吸')->change(); // abilityのデフォルトを炎の呼吸に設定
        });
    }
```
同じように記述してmigrationを実行すると

```php:エラー
  Unknown column type "enum" requested. Any Doctrine type that you use has to be registered with \Doctrine\DBAL\Types\Type::addType(). You can get a list of all the known types with \Doctrine\DBAL\Types\Type::getTypesMap(). If this error occurs during database introspection then you might have forgotten to register all database types for a Doctrine Type. Use AbstractPlatform#registerDoctrineTypeMapping() or have your custom types implement Type#getMappedDatabaseTypes(). If the type name is empty you might have a problem with the cache or forgot some mapping information.
```
このようなエラーを吐きます。これはLaravelの仕様で[ドキュメント](https://readouble.com/laravel/8.x/ja/migrations.html)にも記載があります。
Laravelではenum型のカラム変更をサポートしていません。もしカラムを変更したいのであればSQLを自分で書く必要があります。同じ操作をするのであれば

```php:database\migrations\change_user_table.php
public function up()
    {
        DB::statement("ALTER TABLE users MODIFY ability ENUM ('炎の呼吸','水の呼吸','雷の呼吸') SET DEFAULT '炎の呼吸'");
    }
```
このような記述になります。普段のmigrationに比べるとかなり面倒ですね。
enumのマイグレーションに関しては[このサイト](https://tech.gaogao.asia/create-and-update-enum-column/)で詳しく書かれていました。

https://tech.gaogao.asia/create-and-update-enum-column/

#### 2. 列挙子の取得が難しい
**列挙子**とは今回の例で言うと`'炎の呼吸','水の呼吸','雷の呼吸'`の三つになります。要はenumで選ぶことのできる選択肢ですね。
WEBページでselectタグなどでユーザーにこのような列挙子を選択してもらうことはよくあるかと思います。その際にDBから選択肢のデータを取得したいと思うのが自然です。しかし、Laravelにはenum型の列挙子を取得する関数が用意されていません。これも自力で実装する必要があります。

```php:App\User.php
public static function getAbiltyOptions(){
        $type = DB::select(DB::raw('SHOW COLUMNS FROM users WHERE Field = "type"'))[0]->Type;
        preg_match('/^enum\((.*)\)$/', $type, $matches);
        $values = array();
        foreach(explode(',', $matches[1]) as $value){
            $values[] = trim($value, "'");
        }
        return $values;
    }
```
Userモデルでこのような記述をしてあげることで`User::getAbiltyOptions()`を呼び出すと`['炎の呼吸','水の呼吸','雷の呼吸']`を取得できます。
[こちらのサイト](https://www.webdevqa.jp.net/ja/php/laravel-eloquent%E3%81%AE%E5%88%97%E6%8C%99%E5%9E%8B%E3%82%AA%E3%83%97%E3%82%B7%E3%83%A7%E3%83%B3%E3%82%92%E5%8F%96%E5%BE%97%E3%81%99%E3%82%8B/1050644126/)を参考にしました。

https://www.webdevqa.jp.net/ja/php/laravel-eloquent%E3%81%AE%E5%88%97%E6%8C%99%E5%9E%8B%E3%82%AA%E3%83%97%E3%82%B7%E3%83%A7%E3%83%B3%E3%82%92%E5%8F%96%E5%BE%97%E3%81%99%E3%82%8B/1050644126/

#### 3. データがあると列挙子を削除できない

最後にして最大のデメリットです。今までの2つは「面倒だけど我慢すれば良い」程度のものでしたが、今回のデメリットは **致命的な欠点** になります。詳しく解説します。
このようなテーブルがあります。

|id|name|ability(enum('炎の呼吸','氷の呼吸','雷の呼吸'))|
|:-:|:-:|:-:|
|1|竈門炭治郎|氷の呼吸|
|2|煉獄杏寿郎|炎の呼吸|
|3|我妻善逸|雷の呼吸|
|4|冨岡義勇|氷の呼吸|

「水」と「氷」を読み間違えてしまい「氷の呼吸」と実装してしまいました。
「氷の呼吸」を「水の呼吸」に修正します。

```php:database\migrations\change_user_table.php
public function up()
    {
        DB::statement("ALTER TABLE users MODIFY ability ENUM ('炎の呼吸','水の呼吸','雷の呼吸')"); // 氷の呼吸を水の呼吸に変更
    }
```
<font color="Red">**エラー**</font>が出ます。

理由は`'炎の呼吸','氷の呼吸','雷の呼吸'`以外のデータが存在しているからです。abilityカラムに`氷の呼吸`が存在している状態で`氷の呼吸`の列挙子を削除することはできません。
もし本番で運用していて、enumの列挙子を変更したい、削除したい、となってもその値を持つデータが一つでもあれば削除変更できません。個人的にこれが一番厄介だと感じました。
ちなみにこれはLaravelの仕様ではなくSQLの仕様です。

# enumを使える場合
これまでのメリット、デメリットを踏まえてまとめていきます。
enumの弱点はカラムの変更にあります。逆に言えば**不変のものに対してはenumを使ってもデメリットが少ないということ**です。

* 都道府県 `('北海道','青森県','岩手県',...)`
* 季節 `('春','夏','秋','冬')`
* 月 `(1, 2, 3, 4, ... 12)`
* 組 `('赤組','白組')`（紅白歌合戦のように例年赤組と白組だけの場合）

など。
これらのパターンであればenumを使っても技術的な問題はほとんど起こらないと思います（enumで実装すべきかは置いておいて）。
# enumを使ってはいけない場合
次にenumを使ってはいけない場合の例を紹介します。
enumを使ってはいけないのは**選択支が変わる可能性があるカラム**です。

* チーム`('楽天イーグルス','阪神タイガース','西武ライオンズ','千葉ロッテマリーンズ',...)`  
→チーム名が変わる可能性がある
* ステータス`(1, 2, 3)`   
→後から追加でステータスが増える場合がある
* 人数規模`('0~10人','11~50人','51~100人','101~200人',...)`   
→後から幅を変更したり、追加するのが困難


など。
基本的に少しでも[列挙子](https://qiita.com/tooooofu24/items/3bcdfbf0da3e8fb274af#2-%E5%88%97%E6%8C%99%E5%AD%90%E3%81%AE%E5%8F%96%E5%BE%97%E3%81%8C%E9%9B%A3%E3%81%97%E3%81%84)を変更する可能性のあるカラムはenumで実装することはお勧めしません。
# まとめ

全体を通して言えるのは**Laravelはenumへのサポートが手薄だ**ということですね。
自分の見つけた限りのenumの使いにくさはまとめましたが、もしかするとまだあるかもしれません。
他にenumの特徴を知っている方、ぜひコメントなどで教えてください。
また、間違っている点などありましたらコメントにてご指摘いただけると幸いです。