@Query 어노테이션을 이용한 필터링
에어비앤비에는 카테고리 기능과 가격, 숙소 유형 등으로 필터링해 주는 필터 기능이 있다.
이번 클론 프로젝트에서는 숙소 유형으로 카테고리 기능을 대신하고, 가격 필터까지 적용해 볼 것이다.
현재 전체 조회에는 Pageable을 이용한 페이징이 구현되어 있기 때문에
어떤 방식으로 필터링 기능을 덧붙일까 고민하다가
최대한 Spring Data JPA의 Query Method를 활용하되, 조금 복잡한 쿼리에는 @Query 어노테이션을 사용해 보기로 했다.
@Query 어노테이션을 이용하면 쿼리를 직접 정의할 수 있다!
RoomRepository
public interface RoomRepository extends JpaRepository<Room, Long> {
Page<Room> findByType(String type, Pageable pageable); // 1) 타입별 필터링
Page<Room> findByPriceBetween(int minPrice, int maxPrice, Pageable pageable); // 2) 가격별 필터링
@Query(countQuery = "select count(*) from room r where (r.price between :minPrice and :maxPrice) and r.type = :type", nativeQuery = true)
Page<Room> findByPriceBetweenAndType(@Param("minPrice") int minPrice,
@Param("maxPrice") int maxPrice,
@Param("type") String type,
Pageable pageable); // 3) 타입+가격별 필터링
}
각각 타입별, 가격별 필터링만 적용했을 때와 전체 필터링이 적용되었을 때를 나눠 메소드를 작성해 주었다.
가격별 필터링까지는 Spring Data JPA의 Query Method를 활용하였고,
전체 필터링이 적용되었을 때는 @Query 메소드를 활용하였다.
먼저, 일반 SQL 쿼리문을 사용하였기 때문에 nativeQuery = true로 설정하고,
@Param 어노테이션을 이용하여 매개변수로 넘어온 값을 쿼리문의 변수로 지정하였다.
가장 어려웠던 점은 @Query로 직접 정의한 쿼리문에 Pageable까지 적용해야 하는 부분이었다.
쿼리 결과를 페이징 하기 위해 Pageble 인터페이스를 사용하려면 별도의 countQuery가 필요하다.
👇 Query Method의 다양한 Keyword
Spring Data JPA
Keyword | Sample | JPQL snippet |
And | findByLastnameAndFirstname | … where x.lastname = ?1 and x.firstname = ?2 |
Or | findByLastnameOrFirstname | … where x.lastname = ?1 or x.firstname = ?2 |
Is, Equals | findByFirstname,findByFirstnameIs,findByFirstnameEquals | … where x.firstname = ?1 |
Between | findByStartDateBetween | … where x.startDate between ?1 and ?2 |
LessThan | findByAgeLessThan | … where x.age < ?1 |
LessThanEqual | findByAgeLessThanEqual | … where x.age <= ?1 |
GreaterThan | findByAgeGreaterThan | … where x.age > ?1 |
GreaterThanEqual | findByAgeGreaterThanEqual | … where x.age >= ?1 |
After | findByStartDateAfter | … where x.startDate > ?1 |
Before | findByStartDateBefore | … where x.startDate < ?1 |
IsNull, Null | findByAge(Is)Null | … where x.age is null |
IsNotNull, NotNull | findByAge(Is)NotNull | … where x.age not null |
Like | findByFirstnameLike | … where x.firstname like ?1 |
NotLike | findByFirstnameNotLike | … where x.firstname not like ?1 |
StartingWith | findByFirstnameStartingWith | … where x.firstname like ?1 (parameter bound with appended %) |
EndingWith | findByFirstnameEndingWith | … where x.firstname like ?1 (parameter bound with prepended %) |
Containing | findByFirstnameContaining | … where x.firstname like ?1 (parameter bound wrapped in %) |
OrderBy | findByAgeOrderByLastnameDesc | … where x.age = ?1 order by x.lastname desc |
Not | findByLastnameNot | … where x.lastname <> ?1 |
In | findByAgeIn(Collection<Age> ages) | … where x.age in ?1 |
NotIn | findByAgeNotIn(Collection<Age> ages) | … where x.age not in ?1 |
True | findByActiveTrue() | … where x.active = true |
False | findByActiveFalse() | … where x.active = false |
IgnoreCase | findByFirstnameIgnoreCase | … where UPPER(x.firstame) = UPPER(?1) |
RoomService
@Service
@RequiredArgsConstructor
public class RoomService {
private final RoomRepository roomRepository;
//숙소 페이징, 필터링
@Transactional(readOnly = true)
public Page<Room> addFilter(Pageable pageable, int minPrice, int maxPrice, String type) {
// pageable은 필수, type, price(기본값 -1)별 필터링
Page<Room> roomList = roomRepository.findAll(pageable); // RequestParam page, size만 있을 때
if (type != null && minPrice == -1 && maxPrice == -1) { // 1) RequestParam type만 있을 때
roomList = roomRepository.findByType(type, pageable);
} else if (type == null && minPrice != -1 && maxPrice != -1) { // 2) RequestParam price만 있을 때
roomList = roomRepository.findByPriceBetween(minPrice, maxPrice, pageable);
} else if (type != null && minPrice != -1 && maxPrice != -1) { // 3) RequestParam type, price 둘 다 있을 때
roomList = roomRepository.findByPriceBetweenAndType(minPrice, maxPrice, type, pageable);
}
return roomList;
}
//숙소 정보 전체 조회
@Transactional(readOnly = true)
public List<RoomResponseDto> getRooms(User user, Pageable pageable, int minPrice, int maxPrice, String type) {
List<RoomResponseDto> roomResponseDto = new ArrayList<>();
for (Room room : addFilter(pageable, minPrice, maxPrice, type)) {
List<String> imageFileList = new ArrayList<>();
for (ImageFile imageFile : room.getImageFileList()) {
imageFileList.add(imageFile.getPath());
}
roomResponseDto.add(new RoomResponseDto(
room,
(checkLike(room.getId(), user)),
imageFileList));
}
return roomResponseDto;
}
}
나눠놓은 쿼리 메소드를 경우에 따라 적용시키기 위해 addFilter 메소드를 작성하여 분기처리를 해주었다.
매개변수로 null이나 -1이 들어오면 해당 필터를 적용하지 않은 것으로 간주했다.
이후 숙소 정보를 전체 조회하는 메소드에서 해당 필터를 거친 데이터를 DTO에 담아 반환하였다.
하지만 이 방식은 필터가 추가될수록 하드 코딩이 되기 때문에(..) 추후에는 Query DSL을 이용하는 것이 좋을 듯싶다.
RoomController
@RestController
@RequiredArgsConstructor
@RequestMapping("/api")
public class RoomController {
private final RoomService roomService;
//숙소 전체 조회
@GetMapping("/rooms")
public ResponseEntity<List<RoomResponseDto>> getRooms(@AuthenticationPrincipal UserDetailsImpl userDetails,
@PageableDefault(sort = "createdAt", direction = Sort.Direction.DESC) Pageable pageable,
@RequestParam(required = false, defaultValue = "-1") int minPrice,
@RequestParam(required = false, defaultValue = "-1") int maxPrice,
@RequestParam(required = false) String type) {
return ResponseEntity.ok(roomService.getRooms(userDetails.getUser(), pageable, minPrice, maxPrice, type));
}
}
@PageDefault 어노테이션을 이용하여 createdAt을 기준으로 내림차순 정렬(=최신순 정렬)을 하도록 설정하였다.
모든 RequestParam을 required = false로 설정하여 필요한 필터만 적용될 수 있도록 하였고,
int 타입은 defaultValue를 -1로 설정하여 Service단에서 해당 Parameter의 포함 여부를 알 수 있도록 하였다.
여기서 @RequestParam 애노테이션을 생략하면 스프링 MVC는 내부에서 required=false를 적용한다.
카테고리 & 필터링 & 무한 스크롤
📚 참고자료
'📝 TIL' 카테고리의 다른 글
[TIL] 7주차 클론 코딩ㅣairbnb 클론 코딩 발표 (0) | 2022.12.30 |
---|---|
[TIL] 7주차 클론 코딩ㅣReact + Spring boot 연동 에러 모음집 (0) | 2022.12.29 |
[TIL] 7주차 클론 코딩ㅣAWS EC2 서버 배포 (2) | 2022.12.26 |
[TIL] 6주차 클론 코딩ㅣJPA Pageable을 이용한 페이징 (2) | 2022.12.25 |
[TIL] 6주차 클론 코딩ㅣ주간 시작 (0) | 2022.12.23 |