正直言って、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_idとtypeをコンバインしたインデックスを追加しなおします。そして、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 | +------+------------------------------------------------+メルマガ購読の申し込みはこちらから。