ぶつかった問題
先日ローカル開発環境を整備している際にマイグレーションが通らないパターンがあることに気づきました。
具体的には開発用データベース全体を一度作り直すコマンドの実行時に既にテーブルが存在すると怒られる。
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
不満な点
今回はこのように対応しましたが、不満な点もあります。
- 親クラスのhandle()メソッドにDROP以外の処理も含まれているので、それらも一緒にhandle()メソッドとしてコピーしてくる必要があり、万が一それらを上書きしてしまった際に望ましくない挙動をする可能性があります。
- Laravelが内部的に
migrate:fresh
コマンドを利用しており、かつデフォルトコネクションのみを操作する目的でこれが使われている場合。
ご使用の際はご自身のユースケースを吟味したうえでご使用ください。
他の達成方法
migrate:fresh
コマンドには--database
オプションが存在するため、コマンド発行側でphp artisan migrate:fresh --database xxxxx
とコネクションを指定しつつ複数回発行する方法でも達成可能です。
今回この方法を取らなかったのは、現在migrate:fresh
コマンドを使用している箇所や今後追加される箇所において、いずれも全てのコネクションに対してDROPを行いたかったこと、およびデフォルトのコネクションのみをDROPの対象とするユースケースを想定していないためです。
まとめ
長くなりましたが、困ったときは実装を確認するのが遠回りなようで一番の近道。
聞いたりググったりすることに加えて、自身が利用しているフレームワークの中身を確認する癖をつけたいものです。