laravel model testing 시 duplicated key 에러 피하기
TL;DR
laravel 의 model factory 기능으로 seeding 시에 unique 컬럼에서 "SQLSTATE[23000]: Integrity constraint violation: 1062" 에러를 피하려면 다음 2가지를 변경하면 됩니다.
- Model Factory의 definition 메서드에 DB 에 있는지 여부를 확인하는 코드를 추가
- factory 메서드에 생성할 모델 갯수를 지정하지 말고 루프를 돌면서 생성
// 1062 error 발생 \App\Models\Company::factory(100)->create(); // Good for ($i = 0; $i < 100; $i++) { \App\Models\Company::factory()->create(); }
개요
개인적으로는 개발할 때 적절한 test 데이타가 중요하다고 생각해서 laravel 의 model factory 기능을 자주 사용하고 있습니다.
faker 데이타를 넣어줘야 할 company 라는 테이블이 있어서 다음과 같이 factory 를 구성했습니다.
class CompanyFactory extends Factory { /** * The name of the factory's corresponding model. * * @var string */ protected $model = Company::class; public function definition() { return [ 'url' => $this->faker->url, 'company_name' => $this->faker->company, 'service_name' => \Faker\Factory::create('en')->catchPhrase, // faker locale 이 ko_KR 일 경우 catchPhrase 가 없음. 'created_at' => $this->faker->dateTimeBetween('-1 months', 'now'), ]; }
이제 시딩 명령어로 fake 데이터를 DB 에 넣어주고 있었는데 간헐적으로 unique 속성을 가진 컬럼인 url 에 중복값이 있다며 "1062 duplicate entry" 오류를 발생했습니다.
php artisan db:seed
vendor\doctrine\dbal\lib\Doctrine\DBAL\Driver\PDO\Exception.php:18 Doctrine\DBAL\Driver\PDO\Exception::("SQLSTATE[23000]: Integrity constraint violation: 1062 Duplicate entry 'http://www.bode.com' for key companies.companies_url_unique'")
faker 에서 생성한 url 컬럼이 중복될 수 있을테니까 Model Factory 에 이미 DB 에 있는 코드를 추가했습니다.
저는 특정 블록으로 점프해야 하는 경우에는 goto 구문을 사용합니다.
class CompanyFactory extends Factory { /** * The name of the factory's corresponding model. * * @var string */ protected $model = Company::class; public function definition() { build_url: $url = $this->faker->url; // DB 에 있으면 다시 생성 if ( Company::where('url', $url)->first() != null) { goto build_url; } return [ 'url' => $this->faker->url, 'company_name' => $this->faker->company, 'service_name' => \Faker\Factory::create('en')->catchPhrase(), // faker locale 이 ko_KR 일 경우 catchPhrase 가 없음. 'created_at' => $this->faker->dateTimeBetween('-1 months', 'now'), ]; }
이제 1062 에러가 해결됐겠다 생각하고 다시 db:seed 명령어를 실행했는데 또 간헐적으로 에러가 발생했습니다.
\App\Models\Company::factory(201)->create();
곰곰히 생각해 보니 하나의 factory() 에서 여러 모델을 생성할 경우 여러 개의 DB session 이 생기고 factory() 가 종료되기전까지는 commit 을 수행하지 않는 것 같다는 추측을 했습니다.
이럴 경우 transaction level 이 READ-COMMITTED 이기 때문에 factory내 다른 세션에서는 SELECT 시 값이 없다고 나오지만 INSERT 에는 오류가 날테니까요.
해결을 위해서는 격리수준을 변경하거나 Insert 시마다 Commit 을 해줘야 할것 같은데 그건 큰일이라 factory() 에서 하나의 모델만 생성하면 종료시에 Commit 이 될테니 문제가 없을거라 가정하고 factory 를 호출하는 seeding 코드를 다음과 같이 변경했습니다.
// 1062 error 발생 // \App\Models\Company::factory(100)->create(); // Good for ($i = 0; $i < 100; $i++) { \App\Models\Company::factory()->create(); }
다시 db:seed 를 실행했더니 정상적으로 테스트 데이터가 생성됐고 1062 에러도 발생하지 않았습니다.