laravel6 MySQL レコードが存在するテーブルにカラムを追加するときに既存レコードの新規カラムの値が何も入らない状態を防ぐ

# 目的

- すでに運用されているテーブルのカラムを新規で追加したときに既存レコードの新規カラムに値が何も入らないことを防ぐ方法をまとめる。

# 何がしたいのか

- usersテーブルに下記のようなデータが格納されているものとする。

    ```
    +----+--------+---------------------+-------------------+--------------------------------------------------------------+----------------+---------------------+---------------------+
    | id | name   | email               | email_verified_at | password                                                     | remember_token | created_at          | updated_at          |
    +----+--------+---------------------+-------------------+--------------------------------------------------------------+----------------+---------------------+---------------------+
    |  1 | miriwo | shunokawa@gmail.com | NULL              | $2y$10$iO/bbQup5jUt9gRjE8yRHeYmMLwWiUe9cB3KuaVuSOm0Rrsqdjaba | NULL           | 2021-01-06 13:58:22 | 2021-01-06 13:58:22 |
    +----+--------+---------------------+-------------------+--------------------------------------------------------------+----------------+---------------------+---------------------+
    ```

- usersテーブルにnameの文字数を保持する「name_string_number」カラムを追加したいと思いカラムを追加のマイグレーションファイルを記載したとする。
- マイグレーションを実行すると「id」が1のレコードの「name_string_number」カラムの値は0が自動的に格納されてしまう。

    ```
    +----+--------+---------------------+-------------------+--------------------------------------------------------------+----------------+---------------------+---------------------+--------------------+
    | id | name   | email               | email_verified_at | password                                                     | remember_token | created_at          | updated_at          | name_string_number |
    +----+--------+---------------------+-------------------+--------------------------------------------------------------+----------------+---------------------+---------------------+--------------------+
    |  1 | miriwo | shunokawa@gmail.com | NULL              | $2y$10$iO/bbQup5jUt9gRjE8yRHeYmMLwWiUe9cB3KuaVuSOm0Rrsqdjaba | NULL           | 2021-01-06 13:58:22 | 2021-01-06 13:58:22 |                  0 |
    +----+--------+---------------------+-------------------+--------------------------------------------------------------+----------------+---------------------+---------------------+--------------------+
    ```

- 先のように既存レコード数が1つならまだ手動でSQLを実行するなどの選択肢もあるだろうが現実的ではない。（人為的ミスやSQLを考える手間もかかる。）ましてや動いているサービスの場合何百何万の既存レコードが存在することがザラにある。しかもリリース前確認環境（検証環境）と本番環境でそれぞれ手動で新規カラムに一つずつデータを格納するとリリースの時間もかかってしまう。
- そのため新規カラム作成時に既存レコードの新規カラムにデータを格納したい。

# 環境

- ハードウェア環境

| 項目 | 情報 |
| --- | --- |
| OS | macOS Catalina(10.15.5) |
| ハードウェア | MacBook Pro (13-inch, 2020, Four Thunderbolt 3 ports) |
| プロセッサ | 2 GHz クアッドコアIntel Core i5 |
| メモリ | 32 GB 3733 MHz LPDDR4 |
| グラフィックス | Intel Iris Plus Graphics 1536 MB |

- ソフトウェア環境

| 項目 | 情報 | 備考 |
| --- | --- | --- |
| PHP バージョン | 7.4.11 | Homebrewを用いてこちらの方法で導入→[Mac HomebrewでPHPをインストールする](https://qiita.com/miriwo/items/cd54077aad7e139cf518) |
| Laravel バージョン | 6.X | commposerを用いてこちらの方法で導入→[Mac Laravelの環境構築を行う](https://qiita.com/miriwo/items/adaaaeecae69d6392e08) |
| MySQLバージョン | 8.0.21 for osx10.15 on x86_64 | Homwbrewを用いてこちらの方法で導入→[Mac HomebrewでMySQLをインストールする](https://qiita.com/miriwo/items/ef6a064bc545fbea29d9) |

# 情報

- 実行するコマンドは特筆しない限り前のコマンドと同じディレクトリで実行するものとする。

# 方法

1. アプリ名ディレクトリで下記コマンドを実行してマイグレーションファイルを作成する。

    ```terminal
    $ php artisan make:migration modify_users_table --table=users
    ```

1. 先に作成したマイグレーションファイル（YYYY_MM_DD_HHMMSS_modify_users_table.php）を開く。
1. 開いたマイグレーションファイルを下記のように修正する。

    ```アプリ名ディレクトリ/database/migrations/YYYY_MM_DD_HHMMSS_modify_users_table.php
    <?php
    
    use Illuminate\Database\Migrations\Migration;
    use Illuminate\Database\Schema\Blueprint;
    use Illuminate\Support\Facades\Schema;
    
    class ModifyUsersTable extends Migration
    {
        /**
         * Run the migrations.
         *
         * @return void
         */
        public function up()
        {
            Schema::table('users', function (Blueprint $table) {
                $table->integer('name_string_number');
            });
        
            \App\User::all()->each(function(\App\User $user){
                $user->name_string_number = mb_strlen($user->name);
                $user->save();
            });
        }
    
        /**
         * Reverse the migrations.
         *
         * @return void
         */
        public function down()
        {
            Schema::table('users', function (Blueprint $table) {
                $table->dropColumn('name_string_number');
            });
        }
    }
    ```

1. 下記コマンドを実行してマイグレーションを実施する。

    ```terminal
    $ php artisan migrate
    ```

1. MySQLからテーブルの状態を確認すると既存レコードの新規カラム「name_string_number」にデータが格納されている。

    ```
    +----+--------+---------------------+-------------------+--------------------------------------------------------------+----------------+---------------------+---------------------+--------------------+
    | id | name   | email               | email_verified_at | password                                                     | remember_token | created_at          | updated_at          | name_string_number |
    +----+--------+---------------------+-------------------+--------------------------------------------------------------+----------------+---------------------+---------------------+--------------------+
    |  1 | miriwo | shunokawa@gmail.com | NULL              | $2y$10$iO/bbQup5jUt9gRjE8yRHeYmMLwWiUe9cB3KuaVuSOm0Rrsqdjaba | NULL           | 2021-01-06 13:58:22 | 2021-02-10 06:39:01 |                  6 |
    +----+--------+---------------------+-------------------+--------------------------------------------------------------+----------------+---------------------+---------------------+--------------------+
    ```

1. これでマイグレーションしただけで、既存レコードがある状態で新規カラムを追加しても空もしくはnullなどは格納されずに正しいデータが格納されるようになった。

# 超簡易的な解説

- 今回の肝となる処理はマイグレーションファイルのupメソッドに記載されている下記の部分である。そもそも下記の処理は既存レコードにデータを格納している部分である。なので必ずこの処理は新規カラム追加命令`$table->integer('name_string_number');`よりもあとに記載してあげる必要がある。

    ```php
    \App\User::all()->each(function(\App\User $user){
        $user->name_string_number = mb_strlen($user->name);
        $user->save();
    });
    ```

- `\App\User::all()->each(function(\App\User $user){});`ではUserモデルに紐づくDBの全てのデータを取得しeach()でデータが無くなるまで処理を行っている。（若干違うがforeachで配列を処理するあの動きににている。詳しく知りたい方はこちら→[https://readouble.com/laravel/6.x/ja/collections.html#method-each](https://readouble.com/laravel/6.x/ja/collections.html#method-each)）
- eachで無名関数（クロージャ）を呼び出しその無名関数に引数としてUserモデルに紐づくDBの全てのデータを一つずつ取り出して$userに格納して渡している。
- `$user->name_string_number = mb_strlen($user->name);`ではusersテーブルのレコードそれぞれのnameの値の文字数を$userのインデックスname_string_numberに紐づけている。
- `$user->save();`では書き換えられた$userをDBに保存している。