예시)
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 공부하면서 썼는데
확실히 블로그에 잘 기록해 두면 여러모로 활용하기 좋은 것 같다.
'Spring > JPA' 카테고리의 다른 글
JPA - Lazy 성능 차이, OneToOne 양방향 Lazy (0) | 2024.11.26 |
---|---|
Spring Data JPA - Cascade, orphanRemoval 차이 (0) | 2024.09.24 |
JDBC, JPA, Hibernate, Spring Data JPA (0) | 2023.09.28 |
spring data JPA - slice, page (무한 스크롤, 페이지네이션) (0) | 2023.07.27 |
스프링) JPA Entity option 지연 로딩/영속성 전이/고아 entity삭제 (23-07-12) (0) | 2023.07.14 |