更快的 Laravel 迁移,用于测试具有大量表的项目

Posted

技术标签:

【中文标题】更快的 Laravel 迁移,用于测试具有大量表的项目【英文标题】:Faster Laravel Migration for testing projects with a lot of tables 【发布时间】:2020-12-30 11:57:17 【问题描述】:

我刚刚开始从事这个零测试的大项目。我们的想法是对每个新功能和/或错误进行 TDD,随着时间的推移,我们将增加测试覆盖率。

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

我遇到的问题与性能有关,一个在磁盘中运行的普通 MySql 实例(M.2 SSD)大约需要 90 秒来运行这个大项目的所有迁移。有超过 200 个表要迁移,有很多关系。

这个问题的解决方案是在内存中设置 MySql,使用 tmpfs 和 docker。这个技巧让我将迁移时间减少到只有 10 秒,还不错,但如果你只想运行 1 次测试真的很烦人! 10 秒迁移,几毫秒测试。

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

我刚刚看到这个新功能,它真的让我很开心,非常好!它将帮助很多人并节省大量时间。如果您有很多迁移,您可以显着减少迁移它们的时间。 否则,这并不能解决我的问题。该项目的迁移次数非常接近每个表 1 次迁移。这里不需要优化任何东西。

出于好奇,我拍摄了数据库的 Schema 快照并尝试使用 MySql 命令行恢复它。运行架构恢复并设置所有内容大约需要 3 秒

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

目前,测试数据库一直保持迁移状态,这样我的测试流程(一次测试一次运行)保持超快!

我喜欢认为单个测试应该像一个按钮,你按下它会立即亮绿灯或红灯。

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

我对 MySql 没有深入了解,也许我遗漏了什么......

【问题讨论】:

那是 200 ALTER TABLEs 吗?还是什么? 架构转储文件中没有ALTER TABLE,但有很多FOREIGN KEY CONSTRAINT 上面忘了说: - 我正在使用 InnoDB。模式转储文件(无数据)有 7k 行 SQL。有很多列的很多表。 什么版本的 MySQL? (对于此类 DDL,8.0 可能会更慢。) 为什么不让测试数据库处于迁移状态并为您的测试用例使用事务? 【参考方案1】:

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

它涉及磁盘的操作系统级快照。你会安排在磁盘上只保留 MySQL 数据集,并使用如下所示的 LVM:

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

当准备好重新加载该快照时,使用不同的 LVM 魔法来使用快照而不是磁盘的当前状态。

抱歉,我无法预测需要多少秒,但它根本不涉及 mysqldump。

【讨论】:

【参考方案2】:

只是一个提示

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

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

如果您的大型项目有 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 快照之后可能添加的任何新迁移。这样您就不必在每次添加迁移时都导出迁移的数据库。请记住每隔一段时间准备一个新快照。

【讨论】:

非常感谢!你的小费很有价值。但是,不幸的是,我还不够清楚。上面的代码已经实现了,我是从这篇文章中得到的:How migrations might be slowing down your Laravel tests。这就是为什么我确实尝试直接从终端恢复导出的数据库。如果运行一个测试需要 3 秒,如果从终端恢复需要 3 秒。最后一个性能瓶颈必须在数据库中。我想我达到了极限。很抱歉让您浪费时间。 @JonathanMartins 我从来没有浪费时间去帮助别人,如果有人从我的小费中得到帮助,我会感到很自豪,无论如何,如果它对你有一点帮助,而不是在它之前适当地分配赏金过期了。【参考方案3】:

感谢saddam kamal 和shock_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 秒,下一次测试将以毫秒为单位运行。

【讨论】:

【参考方案4】:

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

这在测试中:

$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]);
    

【讨论】:

以上是关于更快的 Laravel 迁移,用于测试具有大量表的项目的主要内容,如果未能解决你的问题,请参考以下文章

laravel 具有多个迁移表的多个数据库

Laravel 每次迁移多个表

生成为 Laravel 作业队列创建表的迁移?

Laravel 5.5 对具有不同列名的单独表的唯一验证规则

Laravel 基本任务笔记

如何在没有工匠的情况下运行 laravel 迁移(使用代码)