加快Laravel迁移,以测试具有很多表的项目

问题描述

我刚刚开始处理这个测试为零的大项目。这个想法是TDD每一个功能和/或错误,随着时间的推移,我们将增加测试范围。

我不使用sqlite内存数据库进行测试。我确实更喜欢使用MysqL,因为它与我在生产中使用的数据库相同。通常,在小项目中,这没问题,但是在大项目中,是这样!

我遇到的问题与性能有关,一个正常的MysqL实例在磁盘上运行(M.2 SSD)大约需要90秒才能完成此大项目的所有迁移。有200多个表要迁移,并且具有很多关系。

此问题的解决方案是也将tmpfs与docker一起在内存中设置MysqL。这个技巧使我将迁移时间减少到 10秒,还不错,但是如果您只想运行1个测试,那真是令人讨厌!迁移需要10秒,要测试几毫秒。

Laravel 8刚刚引入了一个名为Schema Dump的新功能https://github.com/laravel/framework/pull/32275

我刚刚看到了这个新功能,这真的使我很开心,非常好!这将帮助很多人并节省很多时间。如果您有很多迁移,则可以大大减少迁移所有这些文件的时间。 否则,这不能解决我的问题。该项目的迁移数量非常接近每个表1个迁移。无需在此处进行任何优化。

出于好奇,我对数据库进行了架构快照,并尝试使用MySQL命令行将其还原。花了 3秒来运行模式还原并设置所有内容

 MysqL -h 127.0.0.1 -u root -P 3331 -p default < database/migrations.sql

暂时,测试数据库一直保持迁移状态,这样我的测试流程(一次测试一次运行)就保持了超快的速度!

我想认为单个测试应该像您按下的按钮一样,并立即以绿色或红色点亮。

我的问题:-是否可以减少具有大量表的项目的迁移时间?(仅用于测试)

我没有有关MysqL的内在知识,也许我缺少一些东西...

解决方法

如果目标是在某个时间点加载数据库,而您需要重复使用同一快照,那么我建议您尝试使用LVM快照,而不是“迁移”。

它涉及磁盘的操作系统级快照。您可以将磁盘上仅包含MySQL数据集,并使用LVM来进行类似的操作:

一次设置:停止mysqld,拍摄LVM快照

准备重新加载该快照时,请执行其他LVM魔术操作以使用快照而不是磁盘的当前状态。

对不起,我无法预计将花费几秒钟,但它根本不涉及mysqldump。

,

只是一个提示

当我们具有不同的测试功能时,我也遇到过这种情况,在每次测试后重置数据库通常很有用,这样以前测试的数据不会干扰后续测试。

我采用了以下方法,希望您能从中获得一些帮助,只是分享一些技巧。

如果一个大型项目中有40多个表,则从头开始重写和部署所有迁移可能不是理想的选择。在这种情况下,一种解决方案是将新迁移的数据库导出为SQL“快照”。与原始迁移相比,该包含所有迁移作为原始SQL查询的文件将大大加快解析和执行的速度。 如果您想测试站点的每个功能,那么Laravel's RefreshDatabase trait就很有意义,实际上,它是用来从新迁移的数据库开始每个测试的。在后台,Laravel会在每次测试之前运行以下代码:

protected function refreshTestDatabase()
{
    if (! RefreshDatabaseState::$migrated) {
        $this->artisan('migrate:fresh');
        
        RefreshDatabaseState::$migrated = true;
    }

    $this->beginDatabaseTransaction();
}

如您所见,迁移将仅在第一次测试之前运行一次。第一次测试后,数据库事务将用于快速恢复到初始迁移的数据库状态。在运行多个测试时,这可以节省大量时间。但是,运行这些初始迁移的时间会大大延迟您的测试结果。如果您只想运行一个“快速”测试,这会特别烦人

实施

首先清除数据库,然后使用php artisan migrate:fresh运行迁移。然后打开您的首选数据库客户端并导出(或备份)空数据库。您应该只剩下一个SQL文件。让我们将该文件重命名为migrations_2019_01_10.sql并将其放在应用程序的数据库目录中。 接下来,我们将不得不在RefreshDatabase trait中执行此SQL文件。您可以将整个特征复制到自己的代码库中,也可以直接在自己的TestCase.php中覆盖该方法。最终看起来像这样:

abstract class TestCase extends \Illuminate\Foundation\Testing\TestCase
{
    use CreatesApplication;
    use RefreshDatabase;

    protected function refreshTestDatabase()
    {
        if (! RefreshDatabaseState::$migrated) {
            DB::unprepared(file_get_contents(database_path('migrations_2019_01_10.sql')));

            $this->artisan('migrate');

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

            RefreshDatabaseState::$migrated = true;
        }

        $this->beginDatabaseTransaction();
    }
}

如您所见,我将migrate命令保留在其中,以运行可能在我们的migrations.sql快照之后添加的所有新迁移。这样,您无需在每次添加迁移时都导出已迁移的数据库。只要记住不时准备一次新快照即可。

,

感谢saddam kamalshock_gone_wild触发了一个想法,使我为这个问题清除了思路。我不需要每次运行测试都迁移数据库。在当前的工作流程中,我每天手动迁移所有内容。这可以自动化!

abstract class TestCase extends BaseTestCase
{
    use CreatesApplication;
    use DatabaseTransactions;

    protected function setUp(): void
    {
        parent::setUp();

        // first run of the day,// the database will be migrated to tmpfs
        $result = DB::select(DB::raw("SHOW TABLES LIKE 'users';"));
        if (!count($result))
        {
            $this->artisan('migrate:fresh');
        }
    }
}

我不知道我在想什么!这确实是一段简单的代码,它使所有事情都实现了。由于数据库位于内存(tmpfs)中,因此在一天的第一次测试中只需要迁移一次。第一次运行大约需要10秒钟,下次测试将以毫秒为单位。

,

我使用的方法在初次设置测试时可能会感到笨拙,但对运行测试套件的速度有很大的影响。仅运行该测试所需的那些迁移,而不是运行所有迁移或执行完整的架构导入。例如:

在测试中:

$this->migrate([
    '2016_04_29_132815_create_authors_table','2016_04_29_132815_create_categories_table'
]);
$this->seed(CategoriesTableSeeder::class);

这在TestCase中:

use Artisan;

/**
 * Runs migrations for individual tests
 *
 * @params array $migrations
 * @return void
 */
public function migrate(array $migrations = []): void
{
    $path = database_path('migrations');
    $migrator = app()->make('migrator');
    $migrator->getRepository()->createRepository();
    $files = $migrator->getMigrationFiles($path);

    if (!empty($migrations)) {
        $files = collect($files)->filter(
            function ($value,$key) use ($migrations) {
                if (in_array($key,$migrations)) {
                    return [$key => $value];
                }
            }
        )->all();
    }

    $migrator->requireFiles($files);
    $migrator->runPending($files);
}

/**
 * Runs some or all seeds
 *
 * @params string $seeds
 * @return void
 */
public function seed($seeds = ''): void
{
    $command = "db:seed";

    if (empty($seeds)) {
        Artisan::call($command);
    } else {
        Artisan::call($command,['--class' => $seeds]);
    }
}