3
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 1 year has passed since last update.

よりそうAdvent Calendar 2023

Day 9

[Laravel] migrate:freshコマンドで複数のデータベースのテーブルを全てdropする

Last updated at Posted at 2023-12-08

ぶつかった問題

先日ローカル開発環境を整備している際にマイグレーションが通らないパターンがあることに気づきました。
具体的には開発用データベース全体を一度作り直すコマンドの実行時に既にテーブルが存在すると怒られる。

 SQLSTATE[42S01]: Base table or view already exists: 1050 Table 'users' already exists ... 

migrate:freshコマンド

LaravelではデータベースをDROPし、マイグレーションファイルから作り直すコマンドとしてmigrate:freshコマンドが用意されています。

今回の開発用データベース全体を作り直すコマンドでは、このmigrate:freshコマンド使用していました。
マイグレーション実行前にすべてのテーブルをDROPしているはずなのに何故・・・というのが今回躓いたポイントです。

migrate:freshコマンドで達成できないこと

素直にmigrate:freshコマンドの実装を確認することで原因は判明しました。

1. \Illuminate\Database\Console\Migrations\FreshCommand

php artisan migrate:freshを実行した際に呼ばれる処理の実装は\Illuminate\Database\Console\Migrations\FreshCommandにあります。

中身を確認してみるとすべてのテーブルをDROPしていそうな箇所が存在します。
どうやらdb:wipeコマンドを利用してDROPしているようです。

/**
 * Execute the console command.
 *
 * @return int
 */
public function handle()
{
......()......

    $database = $this->input->getOption('database');

    $this->newLine();

    $this->components->task('Dropping all tables', fn () => $this->callSilent('db:wipe', array_filter([
        '--database' => $database,
        '--drop-views' => $this->option('drop-views'),
        '--drop-types' => $this->option('drop-types'),
        '--force' => true,
    ])) == 0);

......()......
}

2. \Illuminate\Database\Console\WipeCommand

db:wipeコマンドの実装も確認してみます。

$databaseという変数を渡しつつdropAllTablesというメソッドを呼び出していますね。
こちらを確認してみるとどうやら$databaseの文字列を使ってテーブルをDROPするコネクションを指定しているようです。

/**
 * Execute the console command.
 *
 * @return int
 */
public function handle()
{
......()......

    $database = $this->input->getOption('database');


......()......

    $this->dropAllTables($database);


......()......
}

/**
 * Drop all of the database tables.
 *
 * @param  string  $database
 * @return void
 */
protected function dropAllTables($database)
{
    $this->laravel['db']->connection($database)
                ->getSchemaBuilder()
                ->dropAllTables();
}

では、この$databaseを指定しなかったらどうなるのか。

3. \Illuminate\Foundation\Application

\Illuminate\Database\Console\WipeCommand::$laravelの実態である\Illuminate\Foundation\Applicationクラスには下記のようなマッピングが存在し、 \Illuminate\Database\Console\WipeCommand::$laravelのインデックスであるdbには\Illuminate\Database\DatabaseManagerクラスが格納されているようです。

   /**
    * Register the core class aliases in the container.
    *
    * @return void
    */
   public function registerCoreContainerAliases()
   {
       foreach ([
......
           'db' => [\Illuminate\Database\DatabaseManager::class, \Illuminate\Database\ConnectionResolverInterface::class],
......
       ] as $key => $aliases) {
           foreach ($aliases as $alias) {
               $this->alias($key, $alias);
           }
       }
   }

4. \Illuminate\Database\DatabaseManager

このクラスをのぞいてみると、WipeCommandで呼び出されていたconnection()メソッドを見つけることができました。
実際に中身を確認してみるとparseConnectionName()というメソッドの中で、データベース名がnullで来たときはデフォルトのコネクション名を取得していることがわかります。

    /**
     * Get a database connection instance.
     *
     * @param  string|null  $name
     * @return \Illuminate\Database\Connection
     */
    public function connection($name = null)
    {
        [$database, $type] = $this->parseConnectionName($name);
......
        return $this->connections[$name];
    }

    /**
     * Parse the connection into an array of the name and read / write type.
     *
     * @param  string  $name
     * @return array
     */
    protected function parseConnectionName($name)
    {
        $name = $name ?: $this->getDefaultConnection();

        return Str::endsWith($name, ['::read', '::write'])
                            ? explode('::', $name, 2) : [$name, null];
    }

原因まとめ

つまり、migrate:freshコマンドはオプションをつけずに実行した場合、デフォルトのコネクションのデータベースのみをDROPするようです。

【誤】
migrate:freshコマンドを発行すると全てのデータベースのテーブルがDROPされる

【正】
migrate:freshコマンドを発行するとデフォルトコネクションのデータベースのテーブルがDROPされる

解決策

結論から言うと、migrate:freshコマンドの実装部分のうち、実際にDROPを発行している部分をループさせて、全てのコネクションに対してDROPを発行してしまえば良さそうです。

1. Laravelの提供するmigrate:freshコマンドの実装クラスを継承したカスタムコマンドクラスを作成する

<?php

namespace App\Console\Commands\Migrations;

// Laravel提供のmigrate:freshコマンド
use Illuminate\Database\Console\Migrations\FreshCommand as BaseCommand;

// 継承する
class FreshCommand extends BaseCommand

2. 親クラスから持ってきたhandle()メソッドの当該部分をループする

/**
 * Execute the console command.
 *
 * @return int
 */
public function handle()
{
......()......

    $database = $this->input->getOption('database');

    $this->newLine();

    foreach (config('database.connections') as $key => $values) {
        $this->components->task('Dropping all tables in connection : ' . $key, fn () => $this->callSilent('db:wipe', array_filter([
            '--database' => $key,
            '--drop-views' => $this->option('drop-views'),
            '--drop-types' => $this->option('drop-types'),
            '--force' => true,
        ])) == 0);
    }

......()......
}

結果

無事すべてのコネクションのテーブルをDROPすることができました!

$ php artisan migrate:fresh

# これは元々DROPされていた
  Dropping all tables in connection : default .......................................................................................... 234ms DONE
# デフォルトでない他のスキーマもDROPされている
  Dropping all tables in connection : schema2 ............................................................................................. 76ms DONE
  Dropping all tables in connection : schema3 ............................................................................................ 73ms DONE

不満な点

今回はこのように対応しましたが、不満な点もあります。

  1. 親クラスのhandle()メソッドにDROP以外の処理も含まれているので、それらも一緒にhandle()メソッドとしてコピーしてくる必要があり、万が一それらを上書きしてしまった際に望ましくない挙動をする可能性があります。
  2. Laravelが内部的にmigrate:freshコマンドを利用しており、かつデフォルトコネクションのみを操作する目的でこれが使われている場合。

ご使用の際はご自身のユースケースを吟味したうえでご使用ください。

他の達成方法

migrate:freshコマンドには--databaseオプションが存在するため、コマンド発行側でphp artisan migrate:fresh --database xxxxxとコネクションを指定しつつ複数回発行する方法でも達成可能です。

今回この方法を取らなかったのは、現在migrate:freshコマンドを使用している箇所や今後追加される箇所において、いずれも全てのコネクションに対してDROPを行いたかったこと、およびデフォルトのコネクションのみをDROPの対象とするユースケースを想定していないためです。

まとめ

長くなりましたが、困ったときは実装を確認するのが遠回りなようで一番の近道。
聞いたりググったりすることに加えて、自身が利用しているフレームワークの中身を確認する癖をつけたいものです。

3
1
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
3
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?