追記: 2023/10/20
リニューアル記事を書きました!
はじめに
Laravel Advent Calendar 2020 - Qiita の 1日目 の記事です。
明日は @yoshikyoto さんのレガシーなプロダクトに Laravel を導入する第一歩(Laravel DI と Facade)の記事です!
環境
- PHP: 7.4.12
- Laravel: 8.16.1
- MySQL: 8.0.21
- doctrine/dbal: 3.0.0
予備知識
Laravel Seeding(シーディング) とは
Laravelにて標準搭載されている初期データ(テストデータ)を登録する機能です。
Laravel Migration(マイグレーション) とは
Laravelにて標準搭載されているデータベースのテーブル定義の変遷のバージョン管理をしてくれる機能です。
困ったこと
Laravel migrate:fresh コマンド
migrate:fresh
コマンドはすべてのテーブルをドロップして、すべてのマイグレーションを再度実行してくれるコマンドです。
$ php artisan migrate:fresh
# --seed オプションを付けるとシーディングの実行まで行ってくれます。
$ php artisan migrate:fresh --seed
マイグレーションはプロジェクトの開発が進めば進むほど、履歴が長くなってしまいまっさらな状態を作るためにマイグレーションファイルをすべて適応するのはどんどん長くなってきてしまいます。
ダカーポを使ったり、Laravel8で新しく実装されたマイグレーションスカッシングを使う手もありますが、もっと手軽に実行できる方法が我々には求められていました。
Laravel db:seed コマンド
Laravelのシーディングのみ行ってくれるコマンド。
$ php artisan db:seed
テストデータが入った状態でシーディングし直そうとするも、古いレコードが残ったり、外部キー制約でうまく削除できない問題が起こる。
$ php artisan migrate:fresh
で解決するが、マイグレーション適用時間がかかってしまう。
やはり、このコマンドだけでデータをクリアする方法が我々には求められていた。
解決方法
全テーブルを truncate
するシーダーを作ろう。
$ composer require doctrine/dbal
doctrine/dbal
ライブラリが必要です。
テーブル定義を変更する場合には必須なライブラリなので、この機能を求める頃にはおそらく入っているはず...
$ php artisan make:seeder TruncateAllTables
生成された database/seeds/TruncateAllTables.php
ファイルを下記のように上書きします。
<?php declare(strict_types=1);
use Illuminate\Database\Seeder;
class TruncateAllTables extends Seeder
{
/**
* Run the database seeds.
*
* @return void
*/
public function run(): void
{
Schema::disableForeignKeyConstraints();
foreach ($this->getTargetTableNames() as $tableName) {
$this->command->getOutput()->writeln("<comment>Truncating:</comment> {$tableName}");
DB::table($tableName)->truncate();
$this->command->getOutput()->writeln("<info>Truncated:</info> {$tableName}");
}
Schema::enableForeignKeyConstraints();
}
/**
* @return array
*/
private function getTargetTableNames(): array
{
$excludes = ['migrations'];
return array_diff($this->getAllTableNames(), $excludes);
}
/**
* @return array
*/
private function getAllTableNames(): array
{
return DB::connection()->getDoctrineSchemaManager()->listTableNames();
}
}
やってることはシンプルで全テーブル名を取得して各テーブルでtruncate
して削除しています。
migrations
テーブルを消す訳には行かないので除外してます。
外部キー制約が張られてあるとレコードを削除できない場合があるので一時的に無効化してます。
テーブル削除後に外部キー制約を有効化してます。
作成した TruncateAllTables
クラスを DatabaseSeeder
クラスに登録します。
<?php declare(strict_types=1);
namespace Database\Seeders;
use Illuminate\Database\Seeder;
class DatabaseSeeder extends Seeder
{
/**
* Seed the application's database.
*
* @return void
*/
public function run(): void
{
$this->call(TruncateAllTables::class);
// シーダーをたくさん書く ...
\App\Models\User::factory(10)->create();
}
}
コマンドを実行すればokです。
$ php artisan db:seed
Seeding: Database\Seeders\TruncateAllTables
Seeded: Database\Seeders\TruncateAllTables (153.14ms)
Database seeding completed successfully.
これでシーディングを実行しても古いレコードが残ったり、外部キー制約で怒られることもなくなりました。我々の完全勝利だ!🍣🍺
ちなみに TruncateAllTables
だけ呼び出したい場合はクラス名をオプション指定して実行すればokです。
$ php artisan db:seed --class=TruncateAllTables
Database seeding completed successfully.
参考
- https://laravel.com/api/8.x/Illuminate/Database/Schema/Builder.html#method_enableForeignKeyConstraints
- https://laravel.com/api/8.x/Illuminate/Database/MySqlConnection.html
- https://github.com/doctrine/dbal/blob/3.0.x/src/Schema/MySQLSchemaManager.php
- https://github.com/doctrine/dbal/blob/3.0.x/src/Schema/AbstractSchemaManager.php#L213-L228
補足: Laravel7 以前のコード例
Laravel8の場合、名前空間が付いてるのでLaravel7以前で利用する場合は注意が必要です。
<?php declare(strict_types=1);
use Illuminate\Database\Seeder;
class DatabaseSeeder extends Seeder
{
/**
* Seed the application's database.
*
* @return void
*/
public function run(): void
{
$this->call(TruncateAllTables::class);
$this->call(UserSeeder::class);
}
}
<?php declare(strict_types=1);
use Illuminate\Database\Seeder;
use Illuminate\Support\Facades\DB;
use Illuminate\Support\Facades\Schema;
class TruncateAllTables extends Seeder
{
/**
* Run the database seeds.
*
* @return void
*/
public function run(): void
{
Schema::disableForeignKeyConstraints();
foreach ($this->getTargetTableNames() as $tableName) {
$this->command->getOutput()->writeln("<comment>Truncating:</comment> {$tableName}");
DB::table($tableName)->truncate();
$this->command->getOutput()->writeln("<info>Truncated:</info> {$tableName}");
}
Schema::enableForeignKeyConstraints();
}
/**
* @return array
*/
private function getTargetTableNames(): array
{
$excludes = ['migrations'];
return array_diff($this->getAllTableNames(), $excludes);
}
/**
* @return array
*/
private function getAllTableNames(): array
{
return DB::connection()->getDoctrineSchemaManager()->listTableNames();
}
}
補足: 速度のお試し
- マイグレーションファイル数: 45
- テーブル数: 24
上記の環境だと約2.5倍ほどの速度差が見られました。
もし実際に試した人がいたらコメント欄で実行時間教えてもらえると嬉しいです。
$ time php artisan migrate:fresh
real 0m5.013s
user 0m0.327s
sys 0m0.178s
約5.0秒
$ time php artisan db:seed --class=TruncateAllTables
real 0m2.274s
user 0m0.273s
sys 0m0.145s
約2.2秒
補足: db:wipe
6.xでdb:wipe
コマンドが追加されましたが、これだとテーブルがdropされてしまうので、マイグレーションを実行しなおさないといけなくなります。
$ php artisan db:wipe