필터링 옵션은 이름, 지역, 가격으로 필터링을 추가하도록 회의에서 정해서, 추가하게 되었답니다 ..!! 리뷰, 찜하기까지 추가해야 한다고 생각하기 때문에 빠르게 구현해서 시험 끝나면 나머지 부분들도 이야기 해봐야겠지만 아무튼! 필터링을 구현해야 합니다.
필터링을 위해 처음에는 JPA repository에 함수를 다 추가하려 했으나 .. 페이지네이션으로 첫 페이지 / 그 이후 페이지에 대한 쿼리문으로 인해 최소 2개가 발생하고, 필터링 옵션 유무에 따라서도 나뉘기 때문에 4개의 메서드가 더 정의되어야 합니다. 너무 비효율적이고, 중복 코드가 너무 많죠.
그래서 동적으로 추가하기 위해 JPA의 Specification 을 이용해보았습니다. JDBC를 이용해서 쿼리를 직접 작성할 수도 있었고, JPQL을 이용해도 해결 가능했지만, JPA를 사용하기로 했기 때문에 JPA의 기능들을 최대한 활용하는 것이 옳다고 생각하여 도저히 못할 것 같은 경우에만 쿼리를 사용하기로 다짐했었습니다. 그래서 JPA 기능을 활용합니다.
@Repository
public interface PortfolioJPARepository extends JpaRepository<Portfolio, Long>, JpaSpecificationExecutor<Portfolio> {
@EntityGraph("PortfolioWithPlanner")
Page<Portfolio> findAll(@NotNull Specification specification, @Nullable Pageable pageable);
@EntityGraph("PortfolioWithPlanner")
List<Portfolio> findAllByIdLessThanOrderByIdDesc(Long id, Pageable pageable);
@EntityGraph("PortfolioWithPlanner")
List<Portfolio> findAllByOrderByIdDesc(Pageable pageable);
void deleteByPlanner(Planner planner);
@Query("select p from Portfolio p where p.planner.id = :plannerId")
Optional<Portfolio> findByPlannerId(@Param("plannerId") Long plannerId);
}
이렇게 JpaRepository와 함께 JpaSpecificationExecuter도 상속받습니다. 그러면 Specification을 이용한 조회가 가능합니다.
Specification을 위한 객체를 추가합니다. 필터링 옵션은 이름, 지역의 경우 equal 이어야 하고, 가격의 경우 범위이므로 lessThanOrEqualTo, greaterThanOrEqualTo 를 사용해야 합니다. 이렇게 다른 조건들을 필터링 옵션이 존재할 경우에만 쿼리를 추가하도록 해줍니다.
public class PortfolioSpecification {
public static Specification<Portfolio> findPortfolio(Long cursor, Map<String, String> equalKeys, Long minPrice, Long maxPrice) {
return ((root, query, criteriaBuilder) -> {
// 조건절을 담을 배열
List<Predicate> predicates = new ArrayList<>();
// 시작 커서라면 id 조건은 제외
if (!cursor.equals(CursorRequest.START_KEY)) {
predicates.add(criteriaBuilder.lessThan(root.get("id"), cursor));
}
// 필터 조건 추가
equalKeys.keySet().forEach(key ->
predicates.add(
criteriaBuilder.equal(root.get(key), equalKeys.get(key))
)
);
if (minPrice != null) {
predicates.add(
criteriaBuilder.greaterThanOrEqualTo(root.get("totalPrice"), minPrice)
);
}
if (maxPrice != null) {
predicates.add(
criteriaBuilder.lessThanOrEqualTo(root.get("totalPrice"), maxPrice)
);
}
return criteriaBuilder.and(predicates.toArray(new Predicate[0]));
});
}
}
이렇게 하면 끝! 동적 쿼리 생성이 끝났습니다. 잘 작동하지만, 갑작스럽게 또다른 이슈가 발생합니다. EntityGraph를 이용해서 join을 진행했는데, count쿼리가 추가로 1회씩 발생합니다. 이전에 페이지 기반 페이지네이션을 할때는 나타나지 않았던 현상이 발생했습니다. 아마 Specification을 사용해서 그런 것 같은데 .. 찾아봐야겠습니다.
쿼리는
Hibernate:
select
p1_0.id,
p1_0.avg_price,
p1_0.career,
p1_0.contract_count,
p1_0.created_at,
p1_0.description,
p1_0.is_active,
p1_0.location,
p1_0.max_price,
p1_0.min_price,
p1_0.partner_company,
p2_0.id,
p2_0.created_at,
p2_0.email,
p2_0.grade,
p2_0.is_active,
p2_0.order_id,
p2_0.password,
p2_0.payed_amount,
p2_0.payed_at,
p2_0.username,
p1_0.title,
p1_0.total_price
from
portfolio_tb p1_0
left join
user_tb p2_0
on p2_0.id=p1_0.planner_id
and (
p2_0.is_active = true
)
where
(
p1_0.is_active = true
)
and 1=1
order by
p1_0.id desc offset ? rows fetch first ? rows only
Hibernate:
select
count(p1_0.id)
from
portfolio_tb p1_0
where
(
p1_0.is_active = true
)
and 1=1
Hibernate:
select
i1_0.id,
i1_0.file_path,
i1_0.file_size,
i1_0.origin_file_name,
i1_0.portfolio_id,
i1_0.thumbnail
from
imageitem_tb i1_0
left join
portfolio_tb p1_0
on p1_0.id=i1_0.portfolio_id
and (
p1_0.is_active = true
)
where
i1_0.thumbnail=?
and i1_0.portfolio_id in (?,?,?,?,?,?,?,?,?,?)
order by
p1_0.created_at desc
이렇게 나타납니다.