querydsl

querydsl 문제 해결 - 2023-02-16

jinheung90 2023. 2. 21. 16:21
  • 초기 버전의 쿼리
    QClickAggregate qClickAggregate = QClickAggregate.clickAggregate;
    QProduct qProduct = QProduct.product;
    QBrand qBrand = QBrand.brand;
		QProductCategory qProductCategory = QProductCategory.productCategory;
    QCategory qCategory = QCategory.category;
    JPAQuery<ProductFittingClickHistoryDTO> query = 
		jpaQueryFactory.select(new QProductClickCountDTO(
            qProduct.id,
            qProduct.thumbnailImageUrl,
            qProduct.nameEn,
            qProduct.nameKo,
            qProduct.productNo,
            qClickAggregate.clickCount.sum().longValue(),
            qBrand.name
        ))
        .from(qProduct)
        .join(qClickAggregate)
        .on(qClickAggregate.productId.eq(qProduct.id))
				.innerJoin(qProduct.productCategories, qProductCategory)
        .innerJoin(qProductCategory.category, qCategory)
        .innerJoin(qProduct.brand, qBrand)
        .where(qClickAggregate.aggregateDate.between(startDate, endDate));
		(기타 검색 조건문, 쿼리 생략)
    return QueryDslUtil.getPage(query.groupBy(qProduct.id), PageRequest.of(page, size));
}
  • 날짜 기간으로 제품을 클릭한 클릭 수를 더해서 정렬한 다음 제품의 정보를 리스트로 보여주는 쿼리였습니다 (검색 조건 카테고리, 상품명 등등)
    • 전체 카테고리도 검색 가능해야 합니다
  • 문제
    • 합계를 더할 때 특정 제품의 특정 날짜 클릭 수가 2배 또는 3배로 더해지던 문제
  • 문제 지점
    • 카테고리가 제품이랑 중간테이블을 낀 1 : n, n : 1의 관계였기 때문에 카테고리가 여러개면 검색이 중복해서 일어나서 더해져서 제품이 가지고 있는 카테고리 개수만큼 클릭수가 더해진 상황
  • 로직 실행 순서
    1. 카테고리로 제품을 검색합니다
    2. 해당 제품들의 아이디를 뽑아 where in 절을 추가 후 집계를 냅니다
    3. 만약 검색조건이 전체 카테고리면 productId가 가지고 있는 카테고리 목록을 다 뽑습니다
    4. 카테고리 목록에 있는 카테고리를 DTO에 넣습니다
  • 고친 쿼리
  • 실행 순서 1번 : 카테고리로 제품을 검색합니다
public List<Long> getProductIdsByCategoryId(Long categoryId) {
        QProduct qProduct = QProduct.product;
        QProductCategory qProductCategory = QProductCategory.productCategory;
        QCategory qCategory = QCategory.category;

        return jpaQueryFactory.select(new QIdDTO(qProduct.id))
            .from(qProduct)
            .innerJoin(qProduct.productCategories, qProductCategory)
            .innerJoin(qProductCategory.category, qCategory)
            .where(qCategory.id.eq(categoryId))
            .fetch().stream().map(IdDTO::getId).collect(Collectors.toList());
}
  • 실행 순서 2, 3번 : 해당 제품들의 아이디를 뽑아 where in 절을 추가 후 집계를 냅니다
	QClickAggregate qClickAggregate = QClickAggregate.clickAggregate;
        QProduct qProduct = QProduct.product;
        QBrand qBrand = QBrand.brand;
        JPAQuery<ProductFittingClickHistoryDTO> query = 
				jpaQueryFactory.select(new QProductClickCountDTO(
                qProduct.id,
                qProduct.thumbnailImageUrl,
                qProduct.nameEn,
                qProduct.nameKo,
                qProduct.productNo,
                qClickAggregate .clickCount.sum().longValue(),
                qBrand.name
            ))
            .from(qProduct)
            .join(qBatchProductFittingHistory)
            .on(qClickAggregate .productId.eq(qProduct.id))
            .innerJoin(qProduct.brand, qBrand)
            .where(qClickAggregate .aggregateDate.between(startDate, endDate));
            **if(categoryId != null && categoryId > 0L) {
                List<Long> productIds = this.getProductIdsByCategoryId(categoryId);
                query = query.where(qProduct.id.in(productIds));
            }**
	return QueryDslUtil.getPage(query.groupBy(qProduct.id), PageRequest.of(page, size));
  • 실행 순서 4 : 만약 검색조건이 전체 카테고리면 productId가 가지고 있는 카테고리 목록을 다 뽑습니다
	QProduct qProduct = QProduct.product;
        QProductCategory qProductCategory = QProductCategory.productCategory;
        QCategory qCategory = QCategory.category;

        return jpaQueryFactory.select(new QIdNameDTO(qProduct.id, qCategory.name))
            .distinct()
            .from(qProduct)
            .innerJoin(qProduct.productCategories, qProductCategory)
            .innerJoin(qProductCategory.category, qCategory)
            .where(qProduct.id.in(productIds))
            .fetch();
  • 실행 순서 5 : 카테고리를 DTO에 넣습니다
List<IdNameDTO> categoryNameAndProductIds = 
		productDetailClickAggregateQuery.getCategoryIdNamesByProductIds(productIds);

Map<Long, ProductFittingClickHistoryDTO> map = result
    .getContent()
    .stream()
    .collect(Collectors.toMap(ProductClickCountDTO::getProductId, a -> a));

for (IdNameDTO idNameDTO: categoryNameAndProductIds
) {
    ProductFittingClickHistoryDTO productFittingClickHistoryDTO = 
				map.get(idNameDTO.getId());
    productFittingClickHistoryDTO.addCategories(idNameDTO.getName());
}
  • querydsl을 쓰면서 고려했던 점들
    • fetch join + page를 쓰는 것을 피했습니다 
    • fetch join을 안하고 연결 객체를 가져다가 쓰는 것을 피했습니다 (n + 1)