Laravel 8+Pest测试中RefreshDatabase引发SQLite数据库锁定问题
解决Laravel 8双连接测试时SQLite锁定及表不存在问题
问题梳理
- 生产环境:同一MariaDB实例下配置两个数据库连接(
mysql和another),通过config/database.php定义 - 测试环境:用单SQLite文件模拟双连接,
phpunit.xml中配置两个连接指向同一SQLite文件 - 关联逻辑:
Project模型(绑定mysql连接)与PollProject模型(绑定another连接),通过mysql库的project_poll_project中间表建立关联 - 测试异常:
- 引入
RefreshDatabasetrait后,beforeEach里执行attach关联操作触发SQLite锁定错误:SQLSTATE[HY000]: General error: 5 database is locked - 切换为独立SQLite文件或
:memory:内存库时,出现表不存在错误:SQLSTATE[HY000]: General error: 1 no such table: project_poll_project - 移除
RefreshDatabase并手动执行迁移后,测试初始化正常
- 引入
可行解决方案
1. 让RefreshDatabase同时处理双连接迁移
RefreshDatabase默认只处理默认数据库连接的迁移,需要重写迁移逻辑,让它同时执行两个连接的迁移:
<?php namespace Tests; use Illuminate\Foundation\Testing\RefreshDatabase; use Illuminate\Support\Facades\Artisan; abstract class TestCase extends \Illuminate\Foundation\Testing\TestCase { use CreatesApplication, RefreshDatabase { RefreshDatabase::migrate as baseMigrate; } protected function migrate() { // 执行默认mysql连接的迁移 $this->baseMigrate(); // 执行another连接的迁移(如果迁移文件单独放在migrations/another目录,就加--path参数,否则去掉) Artisan::call('migrate', [ '--database' => 'another', '--path' => database_path('migrations/another'), '--force' => true, ]); } }
这样就能保证两个连接对应的表都被正确创建,解决独立文件/内存库下表不存在的问题。
2. 解决SQLite锁定问题
SQLite是单文件数据库,跨连接的并发写入容易触发锁竞争,试试这两个方案:
方案A:测试环境禁用外键约束
在测试基类的setUp方法里添加代码,禁用两个连接的外键约束,减少锁竞争:
protected function setUp(): void { parent::setUp(); foreach (['mysql', 'another'] as $conn) { \DB::connection($conn)->statement('PRAGMA foreign_keys = OFF;'); } }
方案B:用内存库并共享连接实例
如果用:memory:内存库,必须让两个连接共享同一个内存实例,否则会出现两个独立的内存库,导致表找不到。
- 先在
phpunit.xml里配置:
<server name="DB_CONNECTION" value="sqlite"/> <server name="DB_DATABASE" value=":memory:"/> <server name="DB_CONNECTION_ANOTHER" value="sqlite"/> <server name="DB_DATABASE_ANOTHER" value=":memory:"/>
- 然后在
config/database.php的another连接配置里添加instance参数,复用默认SQLite实例:
'another' => [ 'driver' => 'sqlite', 'database' => env('DB_DATABASE_ANOTHER', database_path('another.sqlite')), 'prefix' => '', 'foreign_key_constraints' => env('DB_FOREIGN_KEYS', true), 'instance' => 'default', // 共享默认连接的内存实例 ],
这样两个连接共用同一个内存数据库,既避免文件锁定,又能保证所有表都存在。
3. 手动控制迁移与数据重置
如果上述方案还是有问题,直接放弃RefreshDatabase,手动在测试里控制迁移流程:
<?php use Illuminate\Support\Facades\Artisan; beforeEach(function () { // 每次测试前重置两个数据库 Artisan::call('migrate:fresh', [ '--database' => 'mysql', '--force' => true, ]); Artisan::call('migrate:fresh', [ '--database' => 'another', '--force' => true, ]); // 执行模型创建和关联操作 $project = \App\Models\Project::factory()->create(); $pollProject = \App\Models\PollProject::factory()->create(); $project->pollProjects()->attach($pollProject); });
完全手动控制迁移,避免trait默认逻辑带来的冲突。
内容的提问来源于stack exchange,提问作者Kangel
相关产品推荐
相关产品推荐

