이번에는 저번시간에 이어 식구하자 프로젝트의 SNS 서비스의 querydsl
동적쿼리를 검색기능을 구현해보는 과정을 포스팅 해보겠습니다❗️
프로젝트를 진행하면서, 여러 조건에 따라 선택적으로 게시글을 조회하는 요구사항이 생겼습니다. 쉽게 말해 검색 기능을 구현해야했던 것입니다. 요구사항에서 추출한 검색 조건 및 필터는 다음과 같습니다.
(포함) 조건은 주어진 텍스트가 값에 포함되어있다면 조회
(정확히 일치) 조건은 말 그대로 값이 정확히 일치해야 조회한다는 뜻입니다.
위 요구사항대로 검색조건을 구현하기 위해선, SQL의 WHERE
조건절에 'LIKE'
나 '='
연산자를 통해 주어진 조건에 일치하는 데이터만 조회해야합니다.
앞선 검색 조건들이 항상 모두 적용되지는 않고, 보통 일부 조건만 사용하기 때문에 순수 SQL문을 사용한다면 검색 조건에 따라 사용되는 쿼리를 전부 다르게 해주어야한다는 문제점(수고스러움)이 생깁니다.
-- 닉네임, 해시태그 조건으로 검색시
WHERE `nickname` == '조건' AND `hashtag`='조건'
-- 제목, 내용 조건으로 검색시
WHERE `title` LIKE '%조건%' AND `content` Like '%조건%'
조건에 따라 매번 다른 쿼리를 작성하고 조합해야하는 것은 상당히 번거로운 일입니다. QueryDsl에서는 이러한 동적 쿼리를 아주 쉽고 간결하게 작성할 수 있습니다.
member.username.eq("member1") // username = 'member1'
member.username.ne("member1") //username != 'member1'
member.username.contains("member1") // username like '%member1%'
member.username.isNotNull() // username is not null
member.age.in(10, 20) // age in (10,20)
member.age.notIn(10, 20) // age not in (10, 20)
member.age.between(10,30) // between 10, 30
member.age.goe(30) // age >= 30member.age.gt(30) // age > 30
member.age.loe(30) // age <= 30
member.age.lt(30) // age < 30
member.username.like("member%") // like 검색
member.username.contains("member") // like ‘%member%’ 검색
member.username.startsWith("member") // like ‘member%’ 검색
private List<SnsPost> search(final String snsPostTitle) {
return queryFactory
.selectfrom(post)
// where절에 snsPostTitle like 조건만 포함됨
.where(null, snsPost.snsPostTitle.contains(snsPostTitle))
.fetch();
}
where에는 null 값을 넘겨주어도 되지만, BooleanExpression에 null값을 넣을 경우 IllegalArgumentException이 발생합니다.
snsPost.snsPostTitle.contains(null) -> IllegalArgumentException 예외 발생!!
위 주의사항을 반영하여 주어진 검색 값이 없을 경우 null을 반환하는 메서드를 작성하였습니다.
private BooleanExpression snsPostTitleLike(String snsPostTitle) {
return StringUtils.hasText(snsPostTitle) ? snsPost.snsPostTitle.contains(snsPostTitle) : null;
}
// query
private List<Post> search(final String snsPostTitle) {
return queryFactory
.selectfrom(snsPost)
.where(snsPostTitleLike(snsPostTitle))
.fetch();
}
where에 여러 조건을 넘겨주는 방법은 크게 2가지가 있습니다.
저는 1번의 방법을 사용하였습니다. 검색에 사용되는 조건이 많았기 때문에 2번의 방법을 사용하게 될 경우 쿼리가 조인도 하고 프로젝션도 하는데 길어질 것이라 생각했기 때문입니다. 하지만 조건문을 한 눈에 확인할 수 있다는 장점도 있기 때문에 이 부분은 성향에 따라 선택하시면 될 것 같습니다.
@Override
public List<SnsPostResponseDto> search(final Map<String, String> searchCondition) {
return jpaQueryFactory
.select(Projections.constructor(SnsPostResponseDto.class,
snsPost.id,
snsPost.memberNo,
snsPost.snsPostTitle,
snsPost.snsPostContent,
snsPost.createdBy,
snsPost.createdAt,
snsPost.snsLikesCount,
snsPost.snsViewsCount,
list(hashTag.name),
list(image.url)))
.from(snsHashTagMap)
.join(snsHashTagMap.snsPost, snsPost)
.join(snsHashTagMap.hashTag, hashTag)
.join(snsPost.imageList, image)
.where(allCond(searchCondition))
.fetch();
}
// BooleanBuilder 검색 동적 쿼리
private BooleanBuilder allCond(Map<String, String> searchCondition) {
BooleanBuilder builder = new BooleanBuilder();
return builder
.and(snsPostTileLike(searchCondition.getOrDefault(SNSPOSTTITLE.getParamKey(), null)))
.and(hashTagNameEq(searchCondition.getOrDefault(HASHTAGNAME.getParamKey(), null)))
.and(snsPostContentLike(searchCondition.getOrDefault(SNSPOSTCONTENT.getParamKey(), null)))
.and(nicknameEq(searchCondition.getOrDefault(NICKNAME.getParamKey(), null)));
}
// 검색 동적 쿼리 조건1
private BooleanExpression snsPostTileLike(String snsPostTitle) {
return StringUtils.hasText(snsPostTitle) ? snsPost.snsPostTitle.contains(snsPostTitle) : null;
}
// 검색 동적 쿼리 조건2
private BooleanExpression hashTagNameEq(String hashTagName) {
return StringUtils.hasText(hashTagName) ? snsHashTagMap.hashTag.name.eq(hashTagName) : null;
}
// 검색 동적 쿼리 조건3
private BooleanExpression snsPostContentLike(String snsPostContent) {
return StringUtils.hasText(snsPostContent) ? snsPost.snsPostContent.contains(snsPostContent) : null;
}
// 검색 동적 쿼리 조건4
private BooleanExpression nicknameEq(String nickname) {
return StringUtils.hasText(nickname) ? snsPost.createdBy.eq(nickname) : null;
}
@Getter
public enum SearchParam {
HASHTAGNAME("hashTagName"),
SNSPOSTTITLE("snsPostTitle"),
SNSPOSTCONTENT("snsPostContent"),
NICKNAME("nickname");
private final String paramKey;
SearchParam(String paramKey) {
this.paramKey = paramKey;
}
}
https://jojoldu.tistory.com/394
https://velog.io/@foodsmith96/%EA%B9%80%EC%98%81%ED%95%9C-Querydsl-6-%EC%A4%91%EA%B8%89-%EB%AC%B8%EB%B2%95-3-%EB%8F%99%EC%A0%81-%EC%BF%BC%EB%A6%AC-%EC%98%88%EC%A0%9C