JPQL fetch join에서 on절을 사용할 수 있을까?

wannabeking·2022년 7월 4일
0

회고

목록 보기
4/18

결론부터 말하자면 값이 보장되지 않으면 fetch join에서 on절을 사용할 수 없다.

with-clause not allowed on fetched association

사용하려고 한다면 위와 같은 에러를 마주하게 된다.



on절을 사용하고자 한 이유

헤어샵 예약 관리 시스템을 개발하던 중, 해당 날짜, 헤어샵의 디자이너별 예약 가능한 시간을 조회하기 위하여 다음과 같은 문제를 해결해야 했다.

  1. 헤어샵 마다 예약 시작 시간, 마감 시간이 다르다.
  2. 디자이너 마다 해당 날짜의 특정 시간에 예약이 있을 수 있다.
  3. 예약이 있는 시간은 출력하면 안된다.

이 문제를 해결하기 위하여 다음과 같은 쿼리를 생각하였다.

@Query("SELECT DISTINCT d FROM Designer d "
+ "LEFT JOIN FETCH d.reservations r ON r.date=:date "
+ "WHERE d.hairshop.id=:hairshopId")
List<Designer> findByHairshopIdAndDate(Long hairshopId, LocalDate date);

Designer, Reservation은 일 대 다 연관 관계이기 때문에,
Designerreservations필드를 영속화 하여 반환하고 싶었기 때문이다.

단, 해당 날짜의 디자이너가 가지고 있는 예약들만 가져오면 예약 가능한 시간을 출력할 수 있기 때문에 해당 날짜에 예약이 없는 경우는 reservations를 빈 상태로 받아오고 싶어서 on절에 r.date=:date 조건을 추가하고 싶었던 것이다.

현재 상태는 left join을 사용하고 있지만 r.date=:date를 where절에 사용하기 있기 때문에 해당 날짜에 예약이 없는 디자이너는 가져올 수 없다.

하지만 곧바로 에러를 마주치게 되었고 이유는 다음과 같다.

on을 사용하면 join 시점에 Reservation이 필터링 된다.
따라서 모든 컬렉션 데이터를 가져올 수 없고 연관된 엔터티를 한번에 조회해준다는 fetch join과 위반된다.

자세한 이유는 여기를 확인하면 될 것 같다.

쿼리 한번으로 문제를 해결하고 싶었지만, 할 수 없었기 때문에 다른 방법을 고안하였다.



on절 대신 사용한 방법

on절에 넣을 date 조건을 where절에 넣은 다음 쿼리를 사용하였다.

@Query("SELECT DISTINCT d FROM Designer d JOIN FETCH d.reservations r "
+ "WHERE d.hairshop.id = :hairshopId AND r.date=:date")
List<Designer> findByHairshopIdAndDate(Long hairshopId, LocalDate date);

원래 사용하고 싶었던 on절이 붙은 쿼리와 비교하면 다음과 같은 문제가 있다.

해당 날짜에 예약이 없으면 Designer또한 반환되지 않는다.

예약 가능시간 조회에서는 해당 헤어샵의 모든 디자이너들의 가능시간을 출력해야 되므로 모든 Designer의 정보는 필수적인 데이터였다.

따라서 쿼리에서 반환하지 못한 Designer들을 추가해주기 위하여 다음과 같은 코드를 추가하였다.

List<Designer> allDesigners = designerRepository.findByHairshopId(hairshopId);
for (Designer designer : allDesigners) {
    boolean contains = false;
    Long id = designer.getId();
    for (ReservationTimeResponseDto responseDto : responseDtos) {
        if (Objects.equals(responseDto.getDesignerId(), id)) {
            contains = true;
            break;
        }
    }
    if (!contains) {
        responseDtos.add(
            ReservationConverter.toReservationTimeResponseDto(designer, times)
        );
    }
}
  1. findByHairshopId를 사용하여 해당 헤어샵의 모든 디자이너를 allDesigners에 저장한다.
  2. allDesigners를 순회하며 designer의 id가 responseDtos에 있는지 확인한다. (해당 날짜에 예약이 없어서 누락되었을 수 있으므로)
  3. 없으면 Entity to ResponseDto로 변환하여 넣어준다. (해당 날짜에 예약이 없으니 예약 시작 시간 ~ 예약 마감 시간 중 모든 시간이 저장된 times를 넣어서)

물론 더 좋은 방법이 있을 것이다.
프로젝트 마감 기간이 아직 남았으니, 열심히 리팩토링 해볼 예정이다!



profile
내일은 개발왕 😎

0개의 댓글