Notice
Recent Posts
Recent Comments
Link
일 | 월 | 화 | 수 | 목 | 금 | 토 |
---|---|---|---|---|---|---|
1 | ||||||
2 | 3 | 4 | 5 | 6 | 7 | 8 |
9 | 10 | 11 | 12 | 13 | 14 | 15 |
16 | 17 | 18 | 19 | 20 | 21 | 22 |
23 | 24 | 25 | 26 | 27 | 28 |
Tags
- interface
- 소수찾기 java
- AccessToken
- netty
- 우아한 테크러닝
- DI
- OptimisticLock
- 백준
- RefreshToken
- 알고리즘
- 낙관적 락 롤백
- redissonlock aop
- java
- multimodule testcontainers
- 낙관적 락 재시도
- 멀티모듈 테스트컨테이너
- Spring Cloud Gateway
- S3
- spring aop
- @transactional
- TestContainers
- jpa
- springsecurity
- 형상관리
- spring DI
- ObjectOptimisticLockingFailureException
- kotest testcontainers
- Invalid property 'principal.username' of bean class
- aop
- ObjectOptimisticLockingFailureException 처리
Archives
- Today
- Total
조급하면 모래성이 될뿐
Spring Data Jpa Insert 할 때 Select가 나가네.. 본문
문제 상황
- 설계한 Entity의 id가 Auto Increament값이 아니다.
- 생성자가 호출되는 시점에 fk의 조합으로 생성된다.
- makeReservedSeatId 함수에서 만들어진다.
@Entity
@Table(name = "reserved_seat")
public class ReservedSeat {
public static final String ID_SEPARATOR = "_";
@Id
@Column(name = "id")
private String id;
...
private void makeReservedSeatId(Long scheduleId, Long seatId) {
this.id = new StringBuilder()
.append(scheduleId)
.append(ID_SEPARATOR)
.append(seatId)
.toString();
}
public ReservedSeat(Ticket ticket, Seat seat) {
makeReservedSeatId(ticket.getSchedule().getId(), seat.getId());
this.ticket = ticket;
this.seat = seat;
}
}
- 그리고 아무것도 안 하고 ReservedSeat을 save만 하는데 select 쿼리가 날아갔다.
@Test
@DisplayName("insert 하기 전에 select가 나간다.")
void testInsertBeforeSelect() {
Ticket ticket = saveTicket();
Seat seat = saveSeat();
ReservedSeat reservedSeat = new ReservedSeat(ticket, seat);
log.info("=== 1 ===");
reservedSeatRepository.save(reservedSeat);
log.info("=== 2 ===");
}
- 보면 reservedSeat을 insert 하기 전에.. reservedSeat에 select가 날아갔다...
문제의 원인
- 보통 auto increament로 id를 지정하게 되면 id값이 insert 할 때 생성된다.
- 하지만 지금의 경우는, insert 하기 전에 id값이 결정된다.
- 그래서 Spring Data JPA를 통해 save 할 때, @id 필드에 값을 보고 DB에 존재하는 데이터로 간주한다.
- 그다음 update항목이 있는지 확인을 위해 select를 실행하는 것이다.
- 하지만 저장된 게 없으니 이후 insert 또한 날아간다.
데이터가 많아질수록 성능도 저하될 것이다. 1억 개를 저장하려면 1억 번의 select가 나간다..
save 처리방식
- CrudRepository 구현체인 SimpleJpaRepository를 보면 save는 아래와 같이 처리된다.
@Transactional
@Override
public <S extends T> S save(S entity) {
Assert.notNull(entity, "Entity must not be null.");
if (entityInformation.isNew(entity)) {
em.persist(entity);
return entity;
} else {
return em.merge(entity);
}
}
- 새로운 객체 isNew(entity) = true 이면 persist
- 아니면 merge 한다
- merge는 한번 persist -> detach -> persist상태가 될 때를 의미한다.
- 참조
해결방법
- isNew조건에 타게 하면 된다.
- isNew이 true가 되는 기준을 id 존재 여부가 아닌
- 영속성 콘텍스트에서 조회되지 않았을 때
- 영속 상태가 되기 전
- 으로 변경하면 된다.
isNew라는 상태 값을 하나 만들고 default값을 true로 준다. 그러면 처음 객체를 만들 때는 isTrue에 탈것이다.
그다음
- 영속성 콘텍스트에서 조회되지 않았을 때를 판단하기 위해
- 영속성 컨텍스트에서 조회가 되었다면? 상태를 false로 바꾼다.
- 영속 상태가 되기 전을 판단하기 위해
- 영속 상태가 되기 직전에 상태를 false로 바꾼다.
첫 번째를 처리할 수 있는 어노테이션이 @PostLoad 두 번째가 @PrePersist이다.
즉, 상태를 false로 변경하는 메서드를 추가한다.
다음으로 isNew의 기준을 변경하기 위해 Persistable 인터페이스를 구현해서, 오버 라이딩해준다.
추가적으로 해당 인터페이스를 구현함으로써 getId함수도 오버 라이딩해주면 된다.
최종 코드는 아래와 같다.
@Entity
@Table(name = "reserved_seat")
public class ReservedSeat implements Persistable<String> {
@Id
@Column(name = "id")
private String id;
...
@Transient
private boolean isNew = true;
protected ReservedSeat() {
}
private void makeReservedSeatId(Long scheduleId, Long seatId) {
this.id = new StringBuilder()
.append(scheduleId)
.append(ID_SEPARATOR)
.append(seatId)
.toString();
}
public ReservedSeat(Ticket ticket, Seat seat) {
makeReservedSeatId(ticket.getSchedule().getId(), seat.getId());
this.ticket = ticket;
this.seat = seat;
}
@Override
public boolean isNew() {
return isNew;
}
@PrePersist
@PostLoad
void markNotNew() {
this.isNew = false;
}
@Override
public String getId() {
return this.id;
}
...
}
참조
반응형
'TroubleShooting > 데브코스' 카테고리의 다른 글
Service에서 DataIntegrityViolationException을 Catch 못함 (0) | 2022.07.14 |
---|---|
S3 파일 업로드 (0) | 2022.07.11 |
Service에서 다른 Service를 의존하게 된다면 ? (0) | 2022.07.06 |
N + 1 직접 만났다 (0) | 2022.07.01 |
JPQL like에 _ 포함시키기 (0) | 2022.07.01 |