@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를 적용한다.
카테고리 & 필터링 & 무한 스크롤
📚 참고자료
Pagination with Spring Data JPA
A comprehensive guide on learning how to use paginate query results using the Pageable interface in Spring Data JPA.
attacomsian.com
Spring Data JPA에서 Query를 사용하는 방법
쿼리를 자동 생성해준다고? Spring boot를 통해서 개발을 하게 된다면, DB에 데이터를 삽입, 읽기 등 여러 가지 작동을 하기 위해서는 방식이 필요하다. 쿼리를 작동시키는 방식에는 여러 가지 방식
sundries-in-myidea.tistory.com
[JPA] 조건절(where) 메서드
안녕하세요. J4J입니다. 이번 포스팅은 JPA 조건절(where) 메서드에 대해 적어보는 시간을 가져보려고 합니다. 들어가기에 앞서 이전에 작성된 포스팅을 읽고 오시면 더 이해가 잘 되실 겁니다. 2021.
jforj.tistory.com
'📝 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 |