포스트

Spring Data JPA - flush(sql error 1062 sqlstate 23000)

Spring Data JPA - flush(sql error 1062 sqlstate 23000)

이 글은 Tistory에서 이전된 포스팅입니다. 원본은 여기에서 확인할 수 있습니다.

예시)

Vehicle 테이블

   
idbigintPrimary key (Vehicle ID)
license_platevarchar(255)Vehicle’s license plate number
brandvarchar(255)Manufacturer of the vehicle
modelvarchar(255)Vehicle model
yearintYear of manufacture

Engine 테이블

   
idbigintPrimary key (Engine ID)
engine_numbervarchar(255)Engine serial number
engine_modelvarchar(255)Engine model
statusenum(‘error’, ‘running’, ‘stopped’)Current engine status
vehicle_idbigintForeign key referencing Vehicle ID

Engine과 Vehicle은 일대일 관계이고 외래키 주인은 Engine이다.

Engine 테이블에 다음과 같은 데이터가 있다고 하자.

     
idengine_numberengine_modelstatusvehicle_id
1xxxx-xxxxAQ31
2xxxx-xxxxDW22

이때, 1번 Engine(id 1)에서 차량을 2번 Vehicle(id 2)으로 바꾸려고 한다.

Engine과 Vehicle은 일대일 관계이기 때문에 동일한 vehicle_id가 여러 데이터에 존재할 수 없다.

그래서 순서는

  1. 2번 Vehicle과 관계가 있는 2번 Engine에 vehicle_id를 null 처리 (기존에 관계를 끊고)

  2. 1번 Engine에 vehicle_id = 2로 수정

위와 같다.

Spring Data JPA - flush

문제(c)

1
2
3
4
5
6
7
8
9
10
11
12
    @Transactional
    public void patchEngine(PatchVehicleDto dto) {
        Engine engine = engineRepository.findById(dto.getEngineId()).orElseThrow(() -> new IllegalArgumentException("Invalid engineId"));

        Vehicle vehicle = vehicleRepository.findById(dto.getVehicleId()).orElseThrow(() -> new IllegalArgumentException("Invalid vehicleId"));
        engineRepository.findByVehicle(vehicle).ifPresent((v) -> {
            v.setVehicle(null);
            engineRepository.save(v);
        });
        engine.setVehicle(vehicle);
        engineRepository.save(engine);
    }

앞서 말한 흐름을 JPA로 구현했을 때, SQL Error: 1062, SQLState: 23000, Duplicate entry ‘2’ for key ~~ 에러가 나온다.

해결(c)

1
2
3
4
5
6
7
8
9
10
11
12
    @Transactional
    public void patchEngine(PatchVehicleDto dto) {
        Engine engine = engineRepository.findById(dto.getEngineId()).orElseThrow(() -> new IllegalArgumentException("Invalid engineId"));

        Vehicle vehicle = vehicleRepository.findById(dto.getVehicleId()).orElseThrow(() -> new IllegalArgumentException("Invalid vehicleId"));
        engineRepository.findByVehicle(vehicle).ifPresent((v) -> {
            v.setVehicle(null);
            engineRepository.saveAndFlush(v); // 첫 번째 update
        });
        engine.setVehicle(vehicle);
        engineRepository.save(engine); // 두 번째 update
    }

다음과 같이 saveAndFlush(또는 flush)를 사용하면 에러 없이 의도대로 동작한다.

repository.flush는 영속성 컨텍스트 상태가 DB에 동기화되지만, 트랜잭션이 끝날 때까지 확정되지 않은 상태다.

즉, COMMIT 쿼리를 한 건 아니다.

접근, 의문

문제(c)를 다음과 같이 접근했다.

첫 번째 update 쿼리가 안 나가거나,

첫 번째 update 쿼리가 나갔지만 DB에 반영되지 않고 두 번째 쿼리가 나가거나

1
2
3
4
5
6
7
8
    @Transactional
    public void test() {
        Engine engine = new Engine();
        Vehicle vehicle = new Vehicle();
        engine.setVehicle(vehicle);
        vehicleRepository.save(vehicle);  // 1번
        engineRepository.save(engine);  // 2번
    }

예를 들어 위와 같은 코드가 있다면,

1번, 2번 순으로 코드가 있으면 insert 2번

2번, 1번 순으로 코드가 있으면 insert 2번 update 1번

어떤 형태로든 vehicle insert 쿼리가 나가야 COMMIT이 성립될 수 있다.

문제(c)에 쓰인 코드도 마찬가지로 engine update 쿼리가 먼저 나가야 COMMIT이 성립하는 건데

insert는 flush를 사용하지 않아도 되고, update는 flush를 사용해야 한다.

무슨 차이지?

같은 테이블에서 update 할 때는 flush가 없으면 반영을 못하나?

참고

2023.07.13 - [Spring/JPA] - 스프링) JPA persistence/트랜잭션/Entity상태 (23-07-08)

이전에 jpa persistence 공부하면서 썼는데

확실히 블로그에 잘 기록해 두면 여러모로 활용하기 좋은 것 같다.

이 기사는 저작권자의 CC BY-NC 4.0 라이센스를 따릅니다.