59
45

Laravel 全テーブルをTruncateしてからシーディングする方法

Last updated at Posted at 2020-11-30

追記: 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 ファイルを下記のように上書きします。

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 クラスに登録します。

database/seeds/DatabaseSeeder.php
<?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.

参考

補足: Laravel7 以前のコード例

Laravel8の場合、名前空間が付いてるのでLaravel7以前で利用する場合は注意が必要です。

database/seeders/DatabaseSeeder.php
<?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);
    }
}
database/seeds/TruncateAllTables.php
<?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

59
45
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
59
45