본문 바로가기

JPA

[JPA] deleteAll() 수행 후 바로 insert 했을 때 duplicate entry 에러가 발생하는 문제

프로젝트를 진행하면서 아래와 같은 로직을 작성했었습니다. 백준 이메일을 수정했을때 데이터베이스에 저장된 수정되기 전 백준 이메일로 푼 문제 리스트를 전부 삭제한 후 수정할 이메일로 푼 문제리스트를 다시 저장하는 로직입니다. 

 

@Override
@Transactional
public GithubBaekjoonResponseDto updateGithubAndBaekjoon(Long userId, GithubBaekjoonRequestDto requestDto) {

    User user = getUser(userId);
    user.updateEmail(requestDto.getGithub(), requestDto.getBaekjoon());

    solvedacRepository.deleteAllByUserId(user.getId());
    if (user.getBaekjoon() != null && !user.getBaekjoon().isBlank()) commonService.saveProblemList(user);

    return GithubBaekjoonResponseDto.from(user.getGithub(), user.getBaekjoon());
}

 

제가 생각했던건 해당 메소드에서 생성된 sql 쿼리 문이 영속성 컨텍스트의 sql 저장소에 저장되었다가 Transaction이 끝났을때 flush() 메서드를 호출하여 sql문이 순차적으로(updateEmail -> deleteAllByUserId -> saveProblemList) 실행될 것이라고 생각했습니다. 

 

하지만 위 메서드를 테스트해보니 다른 아이디로 바꿨을때는 문제가 되지 않았지만 사용하던 아이디를 그대로 저장할 때는 duplicate entry 에러가 발생했습니다. 원인을 몰라 답답해하고 있었는데 아래 글을 보고 문제를 해결할 수 있었습니다.

https://eocoding.tistory.com/74

 

@Transactional에서 JPA로 delete한 뒤 insert가 안될 때, duplicate entry 에러가 날 때 해결하기

일단 원인과 해결 방법부터 적고 내 사례와 해봤던 시도들을 구체적으로 적는 건 다음 포스팅으로 넘기려고 한다. Spring Data JPA 사용 중에 데이터를 삭제한 뒤 추가하려고 했더니 duplicate entry 에

eocoding.tistory.com

 

결론부터 말하면 hibernate에서 동작하는 sql문에는 순서가 정해져 있기 때문입니다. 당연히 순차적으로 실행될줄 알았는데.... 아래와 같은 우선순위를 갖고 실행된다고 합니다. 가장 우선순위가 높은 sql 문이 select 문이기 때문에 deleteAllByUserId() 메서드를 먼저 작성하였지만 flush() 될때 insert 문이 먼저 실행되었고 아직 동일한 키를 가진 데이터가 DB에 남아있었기 때문에 발생했던 에러였습니다.

 

해결방법

가장 기본적인 해결방법은 EntityManager를 DI 받고 deleteAll() 메서드가 호출된 뒤 flush() 메서드를 호출하여 먼저 DB에 반영하는 방법입니다.

 

@Service
@RequiredArgsConstructor
public class AuthServiceImpl implements AuthService {

    private final EntityManager em;

    @Override
    @Transactional
    public GithubBaekjoonResponseDto updateGithubAndBaekjoon(Long userId, GithubBaekjoonRequestDto requestDto) {

        User user = getUser(userId);
        user.updateEmail(requestDto.getGithub(), requestDto.getBaekjoon());

        solvedacRepository.deleteAllByUserId(user.getId());
        
        // EntityManger flush 호출
        em.flush();
        
        if (user.getBaekjoon() != null && !user.getBaekjoon().isBlank()) commonService.saveProblemList(user);

        return GithubBaekjoonResponseDto.from(user.getGithub(), user.getBaekjoon());
    }
    
}

 

 

위 방법을 사용하니 메서드는 잘 동작하지만 실행했을 때 위와 같이 삭제할 데이터 수만큼 delete 쿼리가 나가는 것을 확인할 수 있었습니다. 그래서 아래와 같이 JPA @Query 어노테이션을 사용해서 직접 jpql로 delete 쿼리를 작성했고 @Modifying 어노테이션을 사용해서 벌크성 delete가 실행되도록 하였습니다. 그리고 flushAutomatically 속성을 true로 지정하여 해당 쿼리를 실행했을 때, flush() 메서드가 자동적으로 호출되도록 하였습니다. 참고로 jpql은 호출됐을때 자동으로 flush() 메서드를 호출하므로 flushAutomatically 속성은 지워도 무방합니다.

 

public interface SolvedacRepository extends JpaRepository<Solvedac, ProblemId>, SolvedacRepositoryCustom {

    @Modifying(flushAutomatically = true)
    @Query("delete from Solvedac s where s.user.id = :userId")
    void deleteAllByUserId(@Param("userId") Long userId);
}

 

이후 동일한 테스트를 수행한 결과 아래와 같이 delete 쿼리가 한번만 나간것을 확인할 수 있었습니다.

 

'JPA' 카테고리의 다른 글

[JPA] 벌크성 수정 쿼리  (0) 2023.04.02
[JPA] 고급 매핑 (상속 관계 매핑, @MappedSuperclass)  (0) 2023.03.13
[Querydsl] 동적쿼리 작성하기  (0) 2023.01.18
[Querydsl] 프로젝션  (0) 2023.01.17
[Querydsl] Querydsl 적용하기  (0) 2023.01.15