✅ QuerydslPredicateExecutor
- 강의 듣던중에 Querydsl Repository 인터페이스를 작성중에QuerydslPredicateExecutor를 사용하는것을 발견했다.
처음보는 거라 내가 안쓰는사이에 새로 생긴건가하고 찾아보게되었고 Querydsl을 안쓴지가 1년이 넘어서 새로 공부하면서 다양한걸 배우게되었다.
public interface ItemFilterRepository extends JpaRepository<ItemFilter, Long>, ItemFilterRepositoryCustom, QuerydslPredicateExecutor<ItemFilter> {
- Spring Data Jpa에 Querydsl을 사용하기 위해 제공되는 기능이다.
- 과거에 조회 쿼리 로직 클래스에 Querydsl을 사용하기 위해implement QuerydslReositorySupport를 쓰던 시절의 관행과 비슷하게 interface인 repository에서 바로 querydsl을 사용하려고 썼던것 같다.
Spring Data Jpa에 Querydsl을 사용하기 위해 제공되는 기능이다.
QuerydslReositorySupport 인터페이스는
1. Querydsl 3.x 버전을 대상으로 만듬
2. Querydsl 4.x에 나온 JPAQueryFactory로 시작할 수 없음
3. select로 시작할 수 없음 (from으로 시작해야함)
4. QueryFactory 를 제공하지 않음
5. 스프링 데이터 Sort 기능이 정상 동작하지 않음 -> 큐 소트를 사용하면 되긴함.
(소팅을 직접해줘야 된다는 의미)
6. 메서드 체인이 끊깁니다.
위와 같은 단점들이 있다.
✅ Querydsl 사용 방법
현재는 총 3가지의 방법으로 querydsl 을 구현 가능하다.
1. CustomRepository 상속
2. QuerydslReositorySupport 상속
3. JpaQueryFactory 주입해서 단독 사용(해당 추천)
사실 3번이 가장 베스트프랙티스인 이유는 상속을 매번 받는 것도 불편하고
실질적으로 작업하는 JpaQueryFactort만 존재하면 되기 때문이다.
물론 하나의 리포지토리로 통합 관리를 하고 싶으면 1번 방법을 사용하면 된다.
🖥️ 1번 방법 코드
public interface ItemFilterRepository extends JpaRepository<ItemFilter, Long>, ItemFilterRepositoryCustom, QuerydslPredicateExecutor<ItemFilter> {
@Query("select i from ItemFilter i join i.filter f where i.name = :name and f.id = :filterId")
List<ItemFilter> findItemFilterByNameAndFilterId(@Param("name") String name, @Param("filterId") Long filterId);
}
🖥️ 3번 방법 코드
@Repository
@RequiredArgsConstructor
public class ItemLikeRepositoryCustom{
private final JPAQueryFactory queryFactory; // 미리 빈 등록
QItemLike itemLike = QItemLike;
QItem item = Qitem.item;
public List<Item> findItemList(String email){
return queryFactory.select(item)
.from(itemLike)
.join(item)
.where(itemLike.member.email.eq(email))
.fetch();
}
}
✏️공부한 내용
QuerydslPredicateExecutor - 기본적으로 제약이 많아서 실무 환경에서는 사용하는것은 힘들다라고 생각
✅ Querydsl 베스트 프랙티스
동적 쿼리는
1.Predicate 를 파라미터로 이용하는 방법
2.BooleanBuidler 를 이용하는 방법
3.Predicate를 상속받은 BooleanExpression을 쓰는 방법
예전에 프로젝트를 할때도 2번방법을 많이 사용했는데 해당방법을 먼저 코드로 보면 다음과같다.
🖥️ 2번 방법 코드
public List<MemberTeamDto> searchByBuilder(MemberSearchCondition condition){
BooleanBuilder builder = new BooleanBuilder();
if (hasText(condition.getUsername())) {
builder.and(member.username.eq(condition.getUsername()));
}
if(hasText(condition.getTeamName())){
builder.and(team.name.eq(condition.getTeamName()));
}
if(condition.getAgeGoe() != null) {
builder.and(member.age.goe(condition.getAgeGoe()));
}
if(condition.getAgeLoe() != null){
builder.and(member.age.loe(condition.getAgeLoe()));
}
return queryFactory
.select(new QMemberTeamDto(
member.id.as("memberId"),
member.username,
member.age,
team.id.as("teamId"),
team.name.as("teamName")
))
.from(member)
.leftJoin(member.team, team)
.where(builder)
.fetch();
}
근데 직관적이지 않고 쿼리가 어떻게 나가는지 모른다.
그래서 개선된 방법을 발견했다 아직 해당코드를 직접 쓴적은 없지만 다음에 사용하게되면 쓰고싶다.
🖥️ 3번 방법 코드
public List<MemberTeamDto> searchByWhere(MemberSearchCondition condition){
return queryFactory
.select(new QMemberTeamDto(
member.id.as("memberId"),
member.username,
member.age,
team.id.as("teamId"),
team.name.as("teamName")
))
.from(member)
.leftJoin(member.team, team)
.where(
usernameEq(condition.getUsername()),
teamNameEq(condition.getTeamName()),
ageGoe(condition.getAgeGoe()),
ageLoe(condition.getAgeLoe())
)
.fetch();
}
private BooleanExpression usernameEq(String username) {
return hasText(username) ? member.username.eq(username) : null;
}
private BooleanExpression teamNameEq(String teamName) {
return hasText(teamName) ? team.name.eq(teamName) : null;
}
private BooleanExpression ageGoe(Integer ageGoe) {
return ageGoe != null ? member.age.goe(ageGoe) : null;
}
private BooleanExpression ageLoe(Integer ageLoe) {
return ageLoe != null ? member.age.loe(ageLoe) : null;
}
private BooleanExpression ageBetween(Integer ageLoe, Integer ageGoe) {
return ageLoe(ageLoe).and(ageGoe(ageGoe));
}
메소드는 많아졌지만 해당 조건문이 어떤 쿼리문을 만들지 직관적이게 되었다.
✏️공부한 내용
1.독립된 QuerydslRepository를 만드는 것이 좋다.
QuerydslPredicateExecutor 상속 X
2.동적 쿼리문을 짤때는 Predicate 보단 BooleanExpression 을 사용하여 안전하게 where 문을 넣자.
✅ Querydsl 생성자 주입
- @QueryProjection 이외에도 다양한방법이 있지만 이것만 써봤었고 이 방법을 가장 추천한다.
public class MemberDto {
private String name;
private int age;
@QueryProjection // 어노테이션 추가
public MemberDto(String name, int age) {
this.name = name;
this.age = age;
}
}
public void findDtoByQueryProjection() {
queryFactory
.select(new QMemberDto(member.name, member.age))
.from(member)
.fetch();
}
✏️공부한 내용
QDto로 생성된 생성자를 사용하는 방법
런타임 에러와 컴파일 에러 모두 잡을 수 있다.
하지만 DTO 특성상 전 계층(Service, Controller)에 데이터를 전달하기 위한 객체인데 모든 사용에서 Querydsl 의존성을 가지게 된다.
✅ 페이징 성능 개선을 위한 No Offset
offset을 이용하는 것은 offset + limit 만큼의 데이터를 읽어야 하기 때문에 뒤로가면 갈수록 중복되게 조회하는 행들이 발생
앞에서 부터 몇번째까지 세어주고 다시 거기서부터 필요한 만큼 세어 준다.
그래서 No Offset을 이용하는 곳이다
SELECT *
FROM items
WHERE 조건문
AND id < 마지막 조회 ID
ORDER BY id desc
LIMIT 페이지 사이즈
🖥️ 실제 코드
public List<MemberDto> noOffset(Long lastMemberId, int limit){
return queryFactory
.select(new QMemberDto(
member.username,
member.age
))
.from(member)
.where(member.username.contains("member")
.and(memberIdLt(lastMemberId)))
.orderBy(member.id.desc())
.limit(limit)
.fetch();
}
private BooleanExpression memberIdLt(Long lastMemberId) {
return lastMemberId != null ? member.id.lt(lastMemberId): null;
}
✏️공부한 내용
다양한 방법의 성능 최적화방법이있지만 하나씩 배우면서 기록을 해두었다가 실제 프로젝트할떄 써먹어봐야겠다
'Back-end > QueryDsl' 카테고리의 다른 글
| [QueryDsl] QueryDsl Spring boot 3 이상 설정 (1) | 2025.05.28 |
|---|