laravel model testing 시 duplicated key 에러 피하기


TL;DR

laravel 의 model factory 기능으로 seeding 시에 unique 컬럼에서 "SQLSTATE[23000]: Integrity constraint violation: 1062" 에러를 피하려면 다음 2가지를 변경하면 됩니다.

  1. Model Factory의 definition 메서드에 DB 에 있는지 여부를 확인하는 코드를 추가
  2. 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 에러도 발생하지 않았습니다.


같이 보기

Ref