正直言って、Laravelのmigration機能は私のLaravelのプロジェクトでは使用したことありません。よく理解していないこともあり、失敗してクライアントのデータベースを空にしてしまうことを考えると悪夢です。とか言ってコマンドラインでSQL文を実行する現状もリスクはそう変わりません。ということで、前回においてmigrationを使用する機会があったので、この際にmigrationの理解を深めたいです。

DBにインデックスを追加

まず、前回においては以下のようなmigrationを作成することで新規のテーブルaddressesを作成しました。

use Illuminate\Support\Facades\Schema;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Database\Migrations\Migration;
 
class CreateAddressesTable extends Migration
{
    /**
     * Run the migrations.
     *
     * @return void
     */
    public function up()
    {
        Schema::create('addresses', function (Blueprint $table) {
            $table->increments('id');
            $table->integer('user_id')->unsigned();
            $table->string('address');
            $table->timestamps();
        });
    }
 
    /**
     * Reverse the migrations.
     *
     * @return void
     */
    public function down()
    {
        Schema::dropIfExists('addresses');
    }
}

そして、以下を実行することで、addressesのテーブルを作成できます。

$ php artisan migrate
Migrating: 2019_01_10_140631_create_addresses_table
Migrated:  2019_01_10_140631_create_addresses_table

しかし、これではusersのテーブルとのjoinにおいてパフォーマンスが良くありません。
例えば、以下のようなクエリを実行するときです。

Psy Shell v0.9.3 (PHP 7.1.14 — cli) by Justin Hileman
>>> User::join('addresses', 'users.id', '=', 'addresses.user_id')->get();

SQL文では以下となります。

mysql> select * from `users` inner join `addresses` on `users`.`id` = `addresses`.`user_id`

しかし、このjoinでは、データベースはマッチするuser_idを探すために毎回addressesのレコードをすべてチェックしなければなりません。レコード数が多くなれば、クエリの実行時間はとても長くなります。

しかし、これは、addresses.user_idにインデックスを追加するだけで簡単に解決できます。

migrationを作成してインデックスを追加してみましょう。

まず、migrationを作成します。

$ php artisan make:migration change_addresses --table=addresses

作成されたファイルを編集します。

use Illuminate\Support\Facades\Schema;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Database\Migrations\Migration;

class ChangeAddressesTable extends Migration
{
    /**
     * Run the migrations.
     *
     * @return void
     */
    public function up()
    {
        Schema::table('addresses', function (Blueprint $table) {
            $table->index('user_id');
        });
    }

    /**
     * Reverse the migrations.
     *
     * @return void
     */
    public function down()
    {
        Schema::table('addresses', function (Blueprint $table) {
            $table->dropIndex('addresses_user_id_index');
        });
    }
}

これだけです。rollbackのためにdown()も定義することを忘れないように。dropIndex()の引数は、DBの項目名ではなく、間に下線文字を入れて、テーブル名と項目名とindexの文字列をコンバインします(例:addresses_user_id_index)。

次は、このmigrationの反映ですが、その前に–pretendのオプションでどんなSQL文が実行されるかチェックしてみます。

$ php artisan migrate --pretend
ChangeAddressesTable: alter table `addresses` add index `addresses_user_id_index`(`user_id`)

いいですね。思い通りです。

実際にmigrateします。

$ php artisan migrate
Migrating: 2019_01_15_132109_change_addresses_table
Migrated:  2019_01_15_132109_change_addresses_table

住所タイプの導入とユニークインデックスの追加

次は、もう少し複雑なケースを見てみましょう。

現在は、1ユーザー(usersの1レコード)において、複数の住所(addressesのレコード)を紐づけることができますが、住所が会社のものか自宅なのか別荘(夢)なのかわかりません。そこで、その情報を保存するために住所タイプなるtypeという項目を追加します。

migrationを作成します。

$ php artisan make:migration change_addresses2 --table=addresses

作成されたファイルを編集します。


use Illuminate\Support\Facades\Schema;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Database\Migrations\Migration;

class ChangeAddresses2Table extends Migration
{
    /**
     * Run the migrations.
     *
     * @return void
     */
    public function up()
    {
        Schema::table('addresses', function (Blueprint $table) {
            // 項目の追加
            $table->string('type')->after('user_id');

            // インデックスを付け直す
            $table->dropIndex('addresses_user_id_index');
            $table->unique(['user_id','type']);
        });
    }

    /**
     * Reverse the migrations.
     *
     * @return void
     */
    public function down()
    {
        Schema::table('addresses', function (Blueprint $table) {
            $table->string('type')->after('user_id');                                                                                                                                                              
                                                                                                                                                                                                                   
            $table->dropIndex('addresses_user_id_index');                                                                                                                                                          
            $table->unique(['user_id','type']);  
        });
    }
}

typeの項目の追加の後には、現在のインデックスを削除して新規にuser_idtypeをコンバインしたインデックスを追加しなおします。そして、1ユーザーに対して住所タイプは同じものを使用できない制限のために、unique()の関数を使用します。

またもや、–pretendでSQL文をチェックします。

$ php artisan migrate --pretend
ChangeAddresses2Table: alter table `addresses` add `type` varchar(191) not null after `user_id`
ChangeAddresses2Table: alter table `addresses` drop index `addresses_user_id_index`
ChangeAddresses2Table: alter table `addresses` add unique `addresses_user_id_type_unique`(`user_id`, `type`)

次は、migrateの実行なのですが、すでにデータがある状態で実行するとレコード重複のエラーが出る可能性あります。とくにすでに1ユーザーに対して複数の住所のレコードがある場合は、それらの項目に新規の項目typeに空が入ってくるからです。

今回は、このエラーを避けるために、migrate:freshを実行します。すべてのテーブルをdropしてから作成したmigrationをすべて実行してくれます。

$ php artisan migrate:fresh
Dropped all tables successfully.
Migration table created successfully.
Migrating: 2014_10_12_000000_create_users_table
Migrated:  2014_10_12_000000_create_users_table
Migrating: 2014_10_12_100000_create_password_resets_table
Migrated:  2014_10_12_100000_create_password_resets_table
Migrating: 2019_01_10_140631_create_addresses_table
Migrated:  2019_01_10_140631_create_addresses_table
Migrating: 2019_01_15_132109_change_addresses_table
Migrated:  2019_01_15_132109_change_addresses_table
Migrating: 2019_01_18_131940_change_addresses2_table
Migrated:  2019_01_18_131940_change_addresses2_table

しかし、実際のプロダクションではできないことですね。データ皆空になってしまうし。その対応は課題としましょう。

フェイクデータの作成

さて、DBテーブルができたところで、前回のようにフェイクデータの作成をしてみたいです。

今回追加したtypeの項目のために、database/factories/AddressFactory.phpを編集します。

use Faker\Generator as Faker;

$factory->define(App\Address::class, function (Faker $faker) {
    return [
       'type'    => $faker->randomElement(['自宅', '会社']),
       'address' => $faker->address
    ];
});

tinkerで実行してみます。

>>> factory(App\User::class,3)->create()->each(function ($user) { $user->addresses()->save(factory(App\Address::class)->make()); });
...
User::join('addresses', 'users.id', '=', 'addresses.user_id')->get();
=> Illuminate\Database\Eloquent\Collection {#2346
     all: [
       App\User {#2307
         id: 1,
         name: "山本 七夏",
         email: "hirokawa.osamu@example.org",
         created_at: "2019-01-19 02:33:51",
         updated_at: "2019-01-19 02:33:51",
         user_id: 1,
         type: "自宅",
         address: "5212837  京都府佐々木市西区近藤町加納9-4-4 コーポ宮沢110号",
       },
       App\User {#2353
         id: 2,
         name: "若松 裕樹",
         email: "tsuda.rika@example.com",
         created_at: "2019-01-19 02:33:51",
         updated_at: "2019-01-19 02:33:51",
         user_id: 2,
         type: "自宅",
         address: "4853444  奈良県佐々木市南区鈴木町廣川8-3-9",
       },
       App\User {#2355
         id: 3,
         name: "江古田 裕太",
         email: "ksato@example.org",
         created_at: "2019-01-19 02:33:51",
         updated_at: "2019-01-19 02:33:51",
         user_id: 3,
         type: "会社",
         address: "8386040  愛知県宮沢市中央区井高町加納4-2-10",
       },
     ],
   }

うまくフェイクデータが作成されましたね。

最後に1つ有用なコマンドとして、migrate:status。現在のmigrationのステータスを一覧できます。左の1列目がすべてYとなっているのは現在あるmigrationがすべて実行された状態です。

$ php artisan migrate:status
+------+------------------------------------------------+
| Ran? | Migration                                      |
+------+------------------------------------------------+
| Y    | 2014_10_12_000000_create_users_table           |
| Y    | 2014_10_12_100000_create_password_resets_table |
| Y    | 2019_01_10_140631_create_addresses_table       |
| Y    | 2019_01_15_132109_change_addresses_table       |
| Y    | 2019_01_18_131940_change_addresses2_table      |
+------+------------------------------------------------+

migrate:rollbackしたなら、すべてNoのNとなります。

+------+------------------------------------------------+
| Ran? | Migration                                      |
+------+------------------------------------------------+
| N    | 2014_10_12_000000_create_users_table           |
| N    | 2014_10_12_100000_create_password_resets_table |
| N    | 2019_01_10_140631_create_addresses_table       |
| N    | 2019_01_15_132109_change_addresses_table       |
| N    | 2019_01_18_131940_change_addresses2_table      |
+------+------------------------------------------------+

By khino