예시)

Vehicle 테이블

id bigint Primary key (Vehicle ID)
license_plate varchar(255) Vehicle's license plate number
brand varchar(255) Manufacturer of the vehicle
model varchar(255) Vehicle model
year int Year of manufacture

 

Engine 테이블

id bigint Primary key (Engine ID)
engine_number varchar(255) Engine serial number
engine_model varchar(255) Engine model
status enum('error', 'running', 'stopped') Current engine status
vehicle_id bigint Foreign key referencing Vehicle ID

 

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

 

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

id engine_number engine_model status vehicle_id
1 xxxx-xxxx AQ 3 1
2 xxxx-xxxx DW 2 2

 

이때, 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)

    @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)

    @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에 반영되지 않고 두 번째 쿼리가 나가거나

    @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/트랜잭션/Entity상태 (23-07-08)

velog에서 이전한 글 입니다. JPA Persistence Context 영속성 컨텍스트 : Entity 객체를 효율적으로 쉽게 관리하기 위해 만들어진 공간이다. 객체의 생명(유지되는 시간)이나 공간(위치)을 유지하고 이동

cornpip.tistory.com

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

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