正直言って、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 | +------+------------------------------------------------+メルマガ購読の申し込みはこちらから。
