만들고자 하는 기능 : [GET] 카페 검색 (지역, 키워드 검색 동적 쿼리)
API 요청 예시 및 설명
https://{{host}}/cafe/search?loc=홍대&keyword=비밀
: 지역이 홍대이면서 방탈출 카페 이름에 비밀이 포함되는 카페 list 반환
https://{{host}}/cafe/search?loc=홍대&loc=건대&keyword=비밀
: 지역이 홍대 또는 건대이면서 카페 이름에 비밀이 포함되는 list 반환
https://{{host}}/cafe/search
: 검색 조건이 없으므로 전체 list 반환
코드 (Github)
https://github.com/maemae22/escape-room/pull/23
feat: #19 - [GET] 카페 검색 (지역, 키워드 검색 동적 쿼리) 개발 (/cafe/search) by maemae22 · Pull Request #23
feat: #19 - JPAQueryFactory를 Spring Bean으로 등록 feat: #19 - CafeDTO에 @QueryProjection 추가 DTO로 조회하기 위하여 @QueryProjection 추가 (프로젝션과 결과 반환) 컴파일러로 타입 체크 가능 (컴파일 에러 캐치 가
github.com
1. JPAQueryFactory를 Spring Bean으로 등록
필수는 아니지만 (Spring Bean으로 등록하지 않고 하는 방법 아래에서 설명) 이게 더 편한 것 같아 나는 Spring Bean으로 등록해서 사용했다 !
@SpringBootApplication
public class EscapeRoomApplication {
public static void main(String[] args) {
SpringApplication.run(EscapeRoomApplication.class, args);
}
@Bean
JPAQueryFactory jpaQueryFactory(EntityManager em) {
return new JPAQueryFactory(em);
}
}
2. 사용할 DTO에 @QueryProjection 어노테이션 추가 (프로젝션과 결과 반환)
결과를 DTO로 반환할 때의 방법으로
Projection.bean(), fields(), constructor() - 프로퍼티 접근, 필드 직접 접근, 생성자 사용
와 같은 다른 방법도 있지만 @QueryProjection을 사용하였다
@QueryProjection 특징
장점 : 컴파일러로 타입 체크 가능 (컴파일 에러 캐치 가능)
단점 : DTO에 QueryDSL 어노테이션을 유지해야함, DTO까지 Q 파일을 생성해야함
@Data
public class CafeDTO {
private Long cafeId;
private String name;
private String phoneNumber;
private String bhours;
private String address;
private String domain;
private String location;
@QueryProjection
public CafeDTO(Long cafeId, String name, String phoneNumber, String bhours, String address, String domain, String location) {
this.cafeId = cafeId;
this.name = name;
this.phoneNumber = phoneNumber;
this.bhours = bhours;
this.address = address;
this.domain = domain;
this.location = location;
}
}
3. Repository 작성 : Where 다중 파라미터 사용으로 카페 검색 동적 쿼리 작성
(1) CafeRepositoryCustom 생성 (interface)
public interface CafeRepositoryCustom {
Page<CafeDTO> cafeSearchPage(List<String> loc, String keyword, Pageable pageable);
}
(2) CafeRepository에서 CafeRepositoryCustom 상속 받기
public interface CafeRepository extends JpaRepository<Cafe, Long>, CafeRepositoryCustom {
}
(3) CafeRepositoryCustom을 구현한 CafeRepositoryCustomImpl 생성 및 QueryDSL을 활용하여 동적 쿼리 코드 작성 (Where 다중 파라미터 사용)
public class CafeRepositoryCustomImpl implements CafeRepositoryCustom {
private final JPAQueryFactory queryFactory;
public CafeRepositoryCustomImpl(JPAQueryFactory queryFactory) {
this.queryFactory = queryFactory;
}
@Override
public Page<CafeDTO> cafeSearchPage(List<String> loc, String keyword, Pageable pageable) {
QueryResults<CafeDTO> searchResults = queryFactory
.select(new QCafeDTO(cafe.id, cafe.name, cafe.phoneNumber,
cafe.bhours, cafe.address, cafe.domain, cafe.location))
.from(cafe)
.where(locIn(loc), keywordContaining(keyword))
.orderBy(cafe.location.asc())
.offset(pageable.getOffset())
.limit(pageable.getPageSize())
.fetchResults();
List<CafeDTO> contents = searchResults.getResults();
long total = searchResults.getTotal();
return new PageImpl<>(contents, pageable, total);
}
private BooleanExpression locIn(List<String> loc) {
return loc == null ? null : ( !loc.isEmpty() ? cafe.location.in(loc) : null );
}
private BooleanExpression keywordContaining(String keyword) {
return hasText(keyword) ? cafe.name.contains(keyword) : null;
}
}
만약 1번의 JPAQueryFactory를 Spring Bean으로 등록하는 과정을 생략하였다면,
public class CafeRepositoryCustomImpl implements CafeRepositoryCustom {
private final JPAQueryFactory queryFactory;
// JPAQueryFactory가 Spring Bean으로 등록되어있을 경우
// public CafeRepositoryCustomImpl(JPAQueryFactory queryFactory) {
// this.queryFactory = queryFactory;
// }
// JPAQueryFactory가 Spring Bean으로 등록되어있지 않을 경우
public CafeRepositoryCustomImpl(EntityManager em) {
this.queryFactory = new JPAQueryFactory(em);
}
이렇게 작성하면 된다 !
동적 쿼리는 Where 다중 파라미터를 사용하여 작성하였다.
- locIn 함수 : 파라미터로 받은 loc(List<String>)이 null이거나 isEmpty()가 아닐 경우, location in ('loc1', 'loc2')으로 where 검색 조건 추가됨
- keywordContaining 함수 : 파라미터로 받은 keyword가 빈 문자열(혹은 null)이 아닐 경우, name like '%keyword%' 으로 where 검색 조건 추가됨
null 을 반환할 경우 따로 where 절이 추가되지 않는다. 따라서 둘 다 null로 들어왔을 경우 전체 검색을 하게 된다 !
- 또한 orderBy를 통해서 location으로 ASC 정렬하여 반환하게 하였다.
4. Service 작성
@Service
@RequiredArgsConstructor
public class CafeService {
private final CafeRepository cafeRepository;
public Page<CafeDTO> cafeSearchList(List<String> loc, String keyword, Pageable pageable) {
return cafeRepository.cafeSearchPage(loc, keyword, pageable);
}
}
5. Controller 작성
@RestController
@RequiredArgsConstructor
public class CafeController {
private final CafeService cafeService;
@GetMapping("/cafe/search")
public Result cafeSearchList(@RequestParam(required = false) List<String> loc,
@RequestParam(required = false) String keyword,
Pageable pageable) {
Page<CafeDTO> cafeSearchList = cafeService.cafeSearchList(loc, keyword, pageable);
return new Result(cafeSearchList);
}
@Data
@AllArgsConstructor
static class Result<T> {
private T data;
}
}
- List<String> loc와 String keyword를 @RequestParam을 통해 파라미터로 받을 수 있음
- @RequestParam의 required 속성을 false로 설정함 (파라미터로 안 들어오는 경우도 있기 때문)
- Result 클래스로 컬렉션을 감싸서 향후 필요한 필드를 추가할 수 있게 함 (확장성 증가)
결과
http://localhost:8080/cafe/search?keyword=넥스트&loc=신촌&loc=강남
성공 !!!! ㅎㅎ
프로젝트에 QueryDSL으로 동적 쿼리를 작성해본 것은 처음이여서 정리해봤다.
다음에는 Join을 사용하여 동적 쿼리를 작성하는 방법을 포스팅할 예정 !
거의 비슷하지만 count Query를 따로 작성하는 점 등 소소한 변화가 있다 ,, !
'escape-room' 카테고리의 다른 글
[Ubuntu] Nginx, Let's Encrypt를 사용하여 HTTPS 설정하기 (+에러 해결) (0) | 2023.05.19 |
---|---|
[AWS] DNS 설정하기 : 도메인과 EC2 탄력적 IP 연결 (with. 가비아) (0) | 2023.05.19 |
[Spring] Required request parameter '인자' for method parameter type String is not present 에러 (0) | 2023.05.19 |
[IntelliJ] 자주 쓰는 IntelliJ 단축키 (0) | 2023.05.18 |
[Spring] Pageable 설정하기 (글로벌 설정, 개별 설정) (0) | 2023.05.18 |