更快的 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 没有深入了解,也许我遗漏了什么......
【问题讨论】:
那是 200ALTER 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 迁移,用于测试具有大量表的项目的主要内容,如果未能解决你的问题,请参考以下文章