not-a-gardener 개발기 11) QueryDsl 리팩토링, repository 구조

메밀·2023년 9월 14일
0

not-a-gardener

목록 보기
11/13

기존 @Query로 JPQL이나 nativeQuery를 사용했던 메소드를 QueryDsl을 통해 리팩토링했다.
(QueryDsl 진작 배울걸 정말 재밌다 😆)

1. 물주기를 기다리는 식물들

1) 기존 메소드

@Query(value = "SELECT * FROM plant p" +
            " INNER JOIN place pl ON p.place_id = pl.place_id" +
            " LEFT JOIN watering w ON p.plant_id = w.plant_id" +
            " WHERE p.gardener_id = :gardenerId AND w.plant_id IS NULL", nativeQuery = true)
    List<Plant> findWaitingForWateringList(@Param("gardenerId") Long gardenerId);

식물 테이블에 물주기 테이블을 LEFT JOIN 후, 물주기 기록이 없는 식물을 받아오는 쿼리다.

2) 테스트 코드

@SpringBootTest
class PlantRepositoryTest {
    @Autowired
    PlantRepository plantRepository;

    @Autowired
    JPAQueryFactory queryFactory;

    @Test
    @DisplayName("물주기를 기다리는 식물들")
    void findWaitingForWateringList() {
        // 파라미터
        long gardenerId = (long) 1;

        // 원본 메소드
        List<Plant> waitings = plantRepository.findWaitingForWateringList(gardenerId);

        // 리팩토링 시작
        List<Plant> waitingsTest = queryFactory
                .selectFrom(plant)
                .join(plant.place, place)
                .leftJoin(plant.waterings, watering)
                .where(plant.gardener.gardenerId.eq(gardenerId)
                        .and(watering.plant.plantId.isNull()))
                .fetch();

        // 비교하기위해 가공
        List<Long> originList = waitings.stream().map(p -> p.getPlantId()).collect(Collectors.toList());
        List<Long> refactoredList = waitingsTest.stream().map(p -> p.getPlantId()).collect(Collectors.toList());

        // PK만 찍어보기
        System.out.println(originList);
        System.out.println(refactoredList);

        Assertions.assertThat(originList.equals(refactoredList));
    }
}

QueryDsl 코드를 작성 후, PK만 가공하여 리스트로 만든 다음 (콘솔에도 한 번 찍어보고) assert로 서로가 서로를 포함하고 있는지 검사했다.

waiting_list_test

결과는 성공😃!!!

2. 물주기 달력 메소드

1) 기존 메소드

@Query(value = "SELECT watering, chemical, plant" +
            " FROM Watering watering" +
            " LEFT JOIN Chemical chemical" +
            " ON watering.chemical = chemical" +
            " JOIN Plant plant" +
            " ON watering.plant = plant" +
            " WHERE plant.gardener.gardenerId = :gardenerId" +
            " AND watering.wateringDate >= :startDate" +
            " AND watering.wateringDate <= :endDate")
    List<Watering> findAllWateringListByGardenerId(@Param("gardenerId") Long gardenerId, @Param("startDate") LocalDate startDate, @Param("endDate") LocalDate endDate);

달력을 채우기 위해 시작일(startDate)과 종료일(endDate)을 구한 뒤 그 사이 날짜의 물주기를 가져오는 쿼리다.

wateringService 내의 메소드로 startDate/endDate을 구했는데, QueryDsl의 after(), before()로 날짜를 비교하기 위해 서비스 로직을 조금 수정했다.

watering_calendar

기존이 23-08-27 <= date <= 23-10-07 조건이었다면,
지금은 23-08-26 < date < 23-10-08 조건이다.

2) 테스트 코드

	@Test
    @DisplayName("물주기 달력 메소드 - 통과")
    void findAllWateringListByGardenerId() {
        // 파라미터 만들기 - 기존 서비스 로직
        LocalDate date = LocalDate.now();
        LocalDate firstDayOfMonth = LocalDate.of(date.getYear(), date.getMonth(), 1);

        LocalDate originStartDate = firstDayOfMonth.minusDays(firstDayOfMonth.getDayOfWeek().getValue() % 7);

        int tmp = 42 - firstDayOfMonth.lengthOfMonth() - firstDayOfMonth.getDayOfWeek().getValue() % 7;
        LocalDate originEndDate =  firstDayOfMonth.plusDays(firstDayOfMonth.lengthOfMonth() - 1 + tmp);

        // 원본 메소드
        List<Watering> waterings = wateringRepository.findAllWateringListByGardenerId((long) 1, originStartDate, originEndDate);

        // 리팩토링 시작
        LocalDate startDate = wateringService.getStartDate(firstDayOfMonth);
        LocalDate endDate = wateringService.getEndDate(firstDayOfMonth);

        List<Watering> result = queryFactory
                .selectFrom(watering)
                .leftJoin(watering.chemical, chemical)
                .join(watering.plant, plant)
                .where(plant.gardener.gardenerId.eq((long) 1)
                        .and(watering.wateringDate.after(startDate))
                        .and(watering.wateringDate.before(endDate))
                )
                .orderBy(watering.wateringDate.asc())
                .fetch();

		// Id만 가공 
        List<Long> origin = waterings.stream().map(w -> w.getWateringId()).collect(Collectors.toList());
        List<Long> refactored = result.stream().map(w -> w.getWateringId()).collect(Collectors.toList());

		// 출력
        System.out.println(origin);
        System.out.println(refactored);

        Assertions.assertThat(origin.equals(refactored));
    }

역시 동일한 방법으로 Id만 추출하여 같은지 검사했다.

watering_calendar_test

2. QueryDsl 리파지토리와 쿼리메소드 리파지토리 통합

의존성을 굳이 늘리고 싶지 않아서 WateringRepositoryCustom 인터페이스를 생성한 뒤, WateringRepositoryCustomImpl에서 QueryDsl 코드를 구현하고, 기존의 WateringRepositoryJpaRepository<Watering, Long>, WateringRepositoryCustom 모두를 상속하도록 했다.

class

대충 이런 그림이다.

3. 이젠 거의 반가운 LazyInitializationException

@Override
    public List<Watering> findAllWateringListByGardenerId(Long gardenerId, LocalDate startDate, LocalDate endDate) {
        return queryFactory
                .selectFrom(watering)
                .leftJoin(watering.chemical, chemical)
                .fetchJoin() // ⭐️ LazyInitializationException
                .join(watering.plant, plant)
                .fetchJoin() // ⭐️ LazyInitializationException
                .where(
                        plant.gardener.gardenerId.eq(gardenerId)
                                .and(watering.wateringDate.after(startDate))
                                .and(watering.wateringDate.before(endDate))
                )
                .orderBy(watering.wateringDate.asc())
                .fetch();
    }

응답용 DTO 생성에 필요하기 때문에 fetchJoin() 안해주면 LazyInitializationException~

0개의 댓글