@Transactional
private void removeDiaryWithSubEntities(Long diaryId) throws TimeoutException {
...
foodRepository.deleteFoodsInIds(foodIds);
dietRepository.deleteDietsInIds(dietIds);
diaryRepository.deleteById(diaryId); //<- 되지 않는 코드
}
이 코드의 deleteById()
는 JPA 기본 제공 메서드이다.
Hibernate: delete from food where food_id in (? , ? , ? , ? , ? , ? , ?)
Hibernate: delete from diet where diet_id in (? , ? , ? , ?)
Hibernate: delete from food where food_id=?
org.springframework.orm.ObjectOptimisticLockingFailureException: Batch update returned unexpected row count from update [0]; actual row count: 0; expected: 1; statement executed: HikariProxyPreparedStatement@1628378220 wrapping prep19: delete from food where food_id=? {1: 1}; nested exception is org.hibernate.StaleStateException: Batch update returned unexpected row count from update [0]; actual row count: 0; expected: 1; statement executed: HikariProxyPreparedStatement@1628378220 wrapping prep19: delete from food where food_id=? {1: 1}
@Transactional
private void removeDiaryWithSubEntities(Long diaryId) throws TimeoutException {
...
foodRepository.deleteFoodsInIds(foodIds);
dietRepository.deleteDietsInIds(dietIds);
diaryRepository.deleteDiaryForBulkDelete(diaryId); //<- 되는 코드
}
Hibernate: delete from food where food_id in (? , ? , ? , ? , ? , ? , ?)
Hibernate: delete from diet where diet_id in (? , ? , ? , ?)
Hibernate: delete from diabetes_diary where diary_id=?
public class DiaryRepositoryImpl implements DiaryRepositoryCustom {
private final JPAQueryFactory jpaQueryFactory;
public DiaryRepositoryImpl(JPAQueryFactory jpaQueryFactory) {
this.jpaQueryFactory = jpaQueryFactory;
}
@Override
public void deleteDiaryForBulkDelete(Long diaryId) {
jpaQueryFactory.delete(QDiabetesDiary.diabetesDiary)
.where(QDiabetesDiary.diabetesDiary.diaryId.eq(diaryId))
.execute();
}
}
Querdsl로 짠 코드이다.
핵심 빼곤 전부 생략한 코드이다.
public class DiabetesDiary {
@OneToMany(mappedBy = "diary", cascade = CascadeType.ALL, fetch = FetchType.LAZY)
private final List<Diet> dietList = new ArrayList<>();
}
public class Diet {
@OneToMany(mappedBy = "diet", orphanRemoval = true, cascade = CascadeType.ALL, fetch = FetchType.LAZY)
private final List<Food> foodList = new ArrayList<>();
}
public class Food {
}
properties를 사용한다면, 다음으로 바꾼다.
logging.level.org.hibernate=TRACE
logging.level.org.hibernate.hql=INFO
logback을 사용한다면, 다음으로 바꾼다.
<configuration>
<appender name="CONSOLE" class="ch.qos.logback.core.ConsoleAppender">
<encoder>
<pattern>%d{HH:mm:ss.SSS} [%thread] %-5level %logger{36} - %msg%n</pattern>
</encoder>
</appender>
<logger name="org.hibernate" level="TRACE">
<appender-ref ref="CONSOLE" />
</logger>
<logger name="org.hibernate.hql" level="INFO">
<appender-ref ref="CONSOLE" />
</logger>
</configuration>
로그 레벨 Trace로 해도 영양가 있는 로그는 없었다.
낙관적 락
이란 트랜잭션 충돌이 일어나지 않을 것이라 보고,
트랜잭션 커밋할 때 충돌이 발생하면 그 때 처리하자는 방식이다.
비관적 락
이란 트랜잭션 충돌이 일어날 것이라 보고, 우선 락을 거는 방식이다.
기본적으로 제공하는 deleteById(diaryId)
는 상위 엔티티를 삭제할 때, cascadeType.all
을 본다. 그래서 하위 엔티티도 삭제하게 된다. 그런데 이미 foodRepository.deleteFoodsInIds(foodIds); dietRepository.deleteDietsInIds(dietIds);
에서 하위 엔티티들이 전부 삭제되어 있었다. DB에도 해당 사항이 반영되어 있는 것이다.
그런데 cascadeType.all
에 의해 하위 엔티티를 삭제해야 하는데, 삭제 이전에 무엇이 필요할까? 바로 조회
다. 조회를 해야하는데 이미 DB에 삭제가 되어 있으니 조회가 되겠나. 그래서 오류가 난 것 같다.
그럼 Querydsl로 짠 코드는 왜 오류가 안 났을까. 내 생각엔 cascadeType.all
을 시행하지 않고, 순수하게 엔티티 하나만 삭제하기 때문에 잘 작동하는 듯 하다.
참고로, @Transactional(isolation = Isolation.SERIALIZABLE)
까지 격리수준을 높여봤는데도 동일한 오류가 발생했었다.