LoginSignup
5
4

More than 3 years have passed since last update.

複数の DB コネクションを使ってる場合に RefreshDatabase が失敗する

Last updated at Posted at 2020-02-18

MySQL と PostgreSQL など、Laravel で複数のコネクションを使ってるときに RefreshDatabase を使ったテストを書くと、二度目以降の実行でエラーになります。

調べてみると、その原因は RefreshDatabase で使われている migrate:fresh にあることが分かりました。1
migrate:fresh では db:wipemigrate の 2 つのコマンドが呼ばれているのですが、db:wipe がデフォルトのコネクションに対してしか実行されないのです。2

RefreshDatabase のコードを見てみると、以下のような使用するコネクションを指定するための仕組みが用意されており、これを使えばうまくいくような気がしたんですけど、ここで指定したコネクションは、トランザクションでのみ使われるようでした。3
DatabaseTransactions にもこれと同じコードがあるので、そちらから派生させたときの名残なんでしょうね。4

src/Illuminate/Foundation/Testing/RefreshDatabase.php
    /**
     * The database connections that should have transactions.
     *
     * @return array
     */
    protected function connectionsToTransact()
    {
        return property_exists($this, 'connectionsToTransact')
                            ? $this->connectionsToTransact : [null];
    }

じゃあ migrate:fresh の方でもこの値を使うようにしたいと思ったのですが、migrate:fresh では db:wipemigrate がセットになってしまっているため、db:wipe を 2 回実行するようにすると、migrate まで 2 回実行されてしまいます。おのれ密結合。

しかも、migarate--database オプションは migrations テーブルを作るデータベースを指定するためのものなので、たとえ複数のコネクションを使っていたとしても、その中からひとつだけ指定すればよいものです。
一方で、db:wipe--database オプションはテーブルを削除するデータベースを指定するためのものなので、複数コネクションを使っている場合は、その全てを指定する必要があります。
というように、それぞれ意味が違うのですが、migrate:fresh では --database オプションで受け取った値を、その両方に渡してしまっています。2

そのため対策としては、migrate:fresh を使うのをやめて、代わりに migrate:refresh を使うようにするか、db:wipemigrate を自分で呼ぶようにするしかなさそうです。
以下に実装例を挙げておきます。

tests/RefreshDatabase.php(refreshを使うパターン)
<?php

namespace Tests;

use Illuminate\Contracts\Console\Kernel;
use Illuminate\Foundation\Testing\RefreshDatabase as BaseTrait;
use Illuminate\Foundation\Testing\RefreshDatabaseState;

trait RefreshDatabase
{
    use BaseTrait;

    /**
     * Refresh a conventional test database.
     *
     * @return void
     */
    protected function refreshTestDatabase()
    {
        if (! RefreshDatabaseState::$migrated) {
            $this->artisan('migrate:refresh');

            $this->app[Kernel::class]->setArtisan(null);

            RefreshDatabaseState::$migrated = true;
        }

        $this->beginDatabaseTransaction();
    }
}
tests/RefreshDatabase.php(wipeとmigrateを自分で呼ぶパターン)
<?php

namespace Tests;

use Illuminate\Contracts\Console\Kernel;
use Illuminate\Foundation\Testing\RefreshDatabase as BaseTrait;
use Illuminate\Foundation\Testing\RefreshDatabaseState;

trait RefreshDatabase
{
    use BaseTrait;

    /**
     * Refresh a conventional test database.
     *
     * @return void
     */
    protected function refreshTestDatabase()
    {
        if (! RefreshDatabaseState::$migrated) {
            foreach ($this->connectionsToTransact() as $database) {
                $this->artisan('db:wipe', array_filter([
                    '--database' => $database,
                    '--drop-views' => $this->shouldDropViews(),
                    '--drop-types' => $this->shouldDropTypes(),
                    '--force' => true,
                ]));
            }

            $this->artisan('migrate', [
                '--force' => true,
            ]);

            $this->app[Kernel::class]->setArtisan(null);

            RefreshDatabaseState::$migrated = true;
        }

        $this->beginDatabaseTransaction();
    }
}
5
4
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
5
4