Querydsl

Single Ko·2023년 6월 14일
0

jpa

목록 보기
7/8

Querydsl의 장점

  • Java JPA의 한계를 넘어서 쿼리를 자바 코드로 작성.(Creiteria는 복잡함에 의해 실용성이 떨어짐)

  • 문법 오류를 컴파일 시점에 확인해줌

  • 동적 쿼리문제를 쉽게 해결 해줌.

  • 익숙하고 쉬운 SQL 문법 형식

스프링부트3 버전 이후

// Querydsl 추가
implementation 'com.querydsl:querydsl-jpa:5.0.0:jakarta'
annotationProcessor "com.querydsl:querydsl apt: ${dependencyManagement.importedProperties['querydsl.version']}:jakarta"
annotationProcessor "jakarta.annotation:jakarta.annotation-api"
annotationProcessor "jakarta.persistence:jakarta.persistence-api"

스프링부트3 버전 이전

ext["hibernate.version"] = "5.6.5.Final"

dependencies {

//Querydsl 추가
implementation 'com.querydsl:querydsl-jpa'
annotationProcessor "com.querydsl:querydsl apt:${dependencyManagement.importedProperties['querydsl.version']}:jpa"
annotationProcessor "jakarta.annotation:jakarta.annotation-api"
annotationProcessor "jakarta.persistence:jakarta.persistence-api"
}

공통

//Querydsl 추가, 자동 생성된 Q클래스 gradle clean으로 제거
clean {
	delete file('src/main/generated')
}

Gradle IntelliJ 사용법
Gradle - Tasks - build - clean
Gradle - Tasks - other - compileQuerydsl

Gradle 콘솔 사용법
./gradlew clean compileQuerydsl

Q 타입 생성 확인
빌드 후, build - generated 안에 QFile이 생겨야 됨. // (git에 올리면 안된다)

QueryDsl

Querydsl vs JPQL , 서로의 쿼리문을 비교해보자

@Autowired EntityManager em;

@Test
public void startJPQL() {
 	//member1을 찾아라.
 	String qlString = "select m from Member m " +
 				   	  "where m.username = :username";
                   
 	Member findMember = em.createQuery(qlString, Member.class)
 			.setParameter("username", "member1")
 			.getSingleResult();
        
 	assertThat(findMember.getUsername()).isEqualTo("member1");
}

@Test
public void startQuerydsl() {
 	//member1을 찾아라.
 	JPAQueryFactory queryFactory = new JPAQueryFactory(em);
 	QMember m = new QMember("m");
 	Member findMember = queryFactory
 		.select(m)
 		.from(m)
 		.where(m.username.eq("member1"))//파라미터 바인딩 처리
 		.fetchOne();
        
 	assertThat(findMember.getUsername()).isEqualTo("member1");
}
  • 쿼리 DSL은 기본적으로 파라미터 바인딩 방식을 사용해서 SQL Enjection 공격에 방어.
  • QMember m = new QMember("m"); 로 사용하고 있지만, 이렇게 사실 쓰지 않고 바로 사용 가능.
  • JPAQueryFactory queryFactory = new JPAQueryFactory(em) 도 생성자로 받으면 한번만 위에 적을 수 있음.

Q-Type의 활용


Member findMember = queryFactory
 		.select(QMember.member)
 		.from(QMember.member)
 		.where(QMember.member.username.eq("member1"))//파라미터 바인딩 처리
 		.fetchOne();

QMember를 static import를 하면 위의 것 보다 더 줄일  수 있다.

 Member findMember = queryFactory
 		.select(member)
 		.from(member)
 		.where(member.username.eq("member1"))//파라미터 바인딩 처리
 		.fetchOne();
  • 만약에 같은 테이블을 조인해야된다면 선언해서 이름을 다르게해서 사용하자.

검색 조건 쿼리

 Member findMember = queryFactory
 	.selectFrom(member)
 	.where(member.username.eq("member1")
 		.and(member.age.eq(10)))
 	.fetchOne();
  • select 절에 특별한 조건이 없다면 selectFrom으로 붙여서 사용 가능

JPQL이 제공하는 모든 검색 조건 제공

member.username.eq("member1") // username = 'member1'
member.username.ne("member1") //username != 'member1'
member.username.eq("member1").not() // username != 'member1'
member.username.isNotNull() //이름이 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 >= 30
member.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%’ 검색

AND 조건을 파라미터로 처리

 .where(member.username.eq("member1"), member.age.eq(10))
  • where() 에 파라미터로 검색조건을 추가하면 AND 조건이 추가됨
  • 이 경우 null 값은 무시 메서드 추출을 활용해서 동적 쿼리를 깔끔하게 만들 수 있음

결과 조회

  • fetch() : 리스트 조회, 데이터 없으면 빈 리스트 반환
List<Member> fetch = queryFactory
 .selectFrom(member)
 .fetch();
  • fetchOne() : 단 건 조회
    • 결과가 없으면 : null
    • 결과가 둘 이상이면 : com.querydsl.core.NonUniqueResultException
//단 건
  Member findMember1 = queryFactory
 .selectFrom(member)
 .fetchOne();
  • fetchFirst() : limit(1).fetchOne()
//처음 한 건 조회
Member findMember2 = queryFactory
 .selectFrom(member)
 .fetchFirst();

// fetchResults() ,fetchCount()는 Deprecated(향후 미지원)

  • fetchResults() : 페이징 정보 포함, total count 쿼리 추가 실행
//페이징에서 사용
QueryResults<Member> results = queryFactory
 .selectFrom(member)
 .fetchResults();
  • fetchCount() : count 쿼리로 변경해서 count 수 조회
//count 쿼리로 변경
long count = queryFactory
 .selectFrom(member)
 .fetchCount();

정렬

List<Member> result = queryFactory
	.selectFrom(member)
    .where(member.age.eq(100))
	.orderBy(member.age.desc(), member.username.asc().nullsLast())
	.fetch();
  • desc() , asc() : 일반 정렬
  • nullsLast() , nullsFirst() : null 데이터 순서 부여

페이징

List<Member> result = queryFactory
	.selectFrom(member)
	.orderBy(member.username.desc())
	.offset(1) //0부터 시작(zero index)
	.limit(2) //최대 2건 조회
	.fetch();

집합 함수

List<Tuple> result = queryFactory
	.select(member.count(),
		    member.age.sum(),
 			member.age.avg(),
 			member.age.max(),
 			member.age.min()
       )
 	.from(member)
 	.fetch();

그룹화

List<Tuple> result = queryFactory
	.select(team.name, member.age.avg())
	.from(member)
	.join(member.team, team)
	.groupBy(team.name)
	f.fetch();
  • groupBy , 그룹화된 결과를 제한하려면 having

조인

기본 조인
조인의 기본 문법은 첫 번째 파라미터에 조인 대상을 지정하고, 두 번째 파라미터에 별칭(alias)으로 사용할 Q 타입을 지정하면 된다.

join(조인 대상, 별칭으로 사용할 Q타입)

List<Member> result = queryFactory
	.selectFrom(member)
    .join(member.team, team)
 	.where(team.name.eq("teamA"))
 	.fetch();
  • join() , innerJoin() : 내부 조인(inner join)
  • leftJoin() : left 외부 조인(left outer join)
  • rightJoin() : rigth 외부 조인(rigth outer join)
  • JPQL의 on 과 성능 최적화를 위한 fetch 조인 제공

on절

  • 조인 대상 필터링
  • 연관관계 없는 엔티티 외부 조인
  1. 조인대상 필터링 : 회원과 팀을 조인하면서, 팀 이름이 teamA인 팀만 조인, 회원은 모두 조회
/**
 * 예) 회원과 팀을 조인하면서, 팀 이름이 teamA인 팀만 조인, 회원은 모두 조회
 * JPQL: SELECT m, t FROM Member m LEFT JOIN m.team t on t.name = 'teamA'
 * SQL: SELECT m.*, t.* FROM Member m LEFT JOIN Team t ON m.TEAM_ID=t.id and t.name='teamA'
 */
 
List<Tuple> result = queryFactory
	.select(member, team)
    .from(member)
 	.leftJoin(member.team, team).on(team.name.eq("teamA"))
 	.fetch();
  1. 연관관계 없는 엔티티 외부 조인 : 예) 회원의 이름과 팀의 이름이 같은 대상 외부 조인
/**
 *
 * 예) 회원의 이름과 팀의 이름이 같은 대상 외부 조인
 * JPQL: SELECT m, t FROM Member m LEFT JOIN Team t on m.username = t.name
 * SQL: SELECT m.*, t.* FROM Member m LEFT JOIN Team t ON m.username = t.name
 */
 
@Test
public void join_on_no_relation() throws Exception {
 	em.persist(new Member("teamA"));
 	em.persist(new Member("teamB"));
 	List<Tuple> result = queryFactory
 		.select(member, team)
 		.from(member)
 		.leftJoin(team).on(member.username.eq(team.name))
 		.fetch();
 	}
}
  • 일반조인: leftJoin(member.team, team)
  • on조인: from(member).leftJoin(team).on(xxx)

페치 조인

QueryDsl에서 페치조인은 어떻게 하는가?

페치 조인은 SQL에서 제공하는 기능은 아니다. SQL조인을 활용해서 연관된 엔티티를 SQL 한번에 조회하는 기능이다. 주로 성능 최적화에 사용하는 방법이다

Member findMember = queryFactory
	.selectFrom(member)
 	.join(member.team, team).fetchJoin()
 	.where(member.username.eq("member1"))
 	.fetchOne();
  • join(), leftJoin() 등 조인 기능 뒤에 fetchJoin() 이라고 추가하면 된다.

서브 쿼리

public void subQuery() throws Exception {

 QMember memberSub = new QMember("memberSub");
 
 List<Member> result = queryFactory
 		.selectFrom(member)
 		.where(member.age.eq(
 			JPAExpressions
 				.select(memberSub.age.max())
 				.from(memberSub)
 		))
 		.fetch();
  • JPAExpressions를 static import 해서 사용 가능하다.

from 절의 서브쿼리 한계
JPA JPQL 서브쿼리의 한계점으로 from 절의 서브쿼리(인라인 뷰)는 지원하지 않는다. 당연히 Querydsl도 지원하지 않는다.

하이버네이트 구현체를 사용하면 select 절의 서브쿼리는 지원한다. Querydsl도 하이버네이트 구현체를 사용하면 select 절의 서브쿼리를 지원한다.

from 절의 서브쿼리 해결방안
1. 서브쿼리를 join으로 변경한다. (가능한 상황도 있고, 불가능한 상황도 있다.)
2. 애플리케이션에서 쿼리를 2번 분리해서 실행한다.
3. nativeSQL을 사용한다.

Case문

단순한 조건

List<String> result = queryFactory
	.select(member.age
		.when(10).then("열살")
		.when(20).then("스무살")
		.otherwise("기타")
     )
	.from(member)
	.fetch();

복잡한 조건

List<String> result = queryFactory
	.select(new CaseBuilder()
		.when(member.age.between(0, 20)).then("0~20살")
 		.when(member.age.between(21, 30)).then("21~30살")
 		.otherwise("기타")
     )
 	.from(member)
 	.fetch();

orderBy에서 Case 문 함께 사용하기 예제

NumberExpression<Integer> rankPath = new CaseBuilder()
	.when(member.age.between(0, 20)).then(2)
    .when(member.age.between(21, 30)).then(1)
 	.otherwise(3);
    
List<Tuple> result = queryFactory
	.select(member.username, member.age, rankPath)
    .from(member)
    .orderBy(rankPath.desc())
 	.fetch();

상수, 문자 더하기

상수가 필요하면 Expressions.constant(xxx) 사용

Tuple result = queryFactory
	.select(member.username, Expressions.constant("A"))
 	.from(member)
 	.fetchFirst();

문자 더하기 concat

String result = queryFactory
	.select(member.username.concat("_").concat(member.age.stringValue()))
    .from(member)
 	.where(member.username.eq("member1"))
 	.fetchOne();
  • stringValue() 를 이용하면 문자로 되서 처리된다.
  • 문자가 아닌 다른 타입들은 stringValue() 로 문자로 변환할 수 있다. 이 방법은 ENUM을 처리할 때도 자주 사용한다.

프로젝션과 결과 반환

기본

프로젝션: select 대상 지정

프로젝션 대상이 하나

List<String> result = queryFactory
	.select(member.username)
	.from(member)
	.fetch();
  • 프로젝션 대상이 하나면 타입을 명확하게 지정할 수 있음
  • 프로젝션 대상이 둘 이상이면 튜플이나 DTO로 조회

튜플 조회

프로젝션 대상이 둘 이상일 때 사용

com.querydsl.core.Tuple

List<Tuple> result = queryFactory
	.select(member.username, member.age)
 	.from(member)
 	.fetch();
 
for (Tuple tuple : result) {
	String username = tuple.get(member.username);
    Integer age = tuple.get(member.age);
    System.out.println("username=" + username);
    System.out.println("age=" + age);
}
  • tuple이 repository를 넘어서서 service나 controller까지 가는건 그렇게 좋은 설계가 아니다.
  • 바깥으로 나갈때는 DTO로 변환해서 나가는게 좋음.

DTO 조회

  • 순수 JPA에서 DTO를 조회할 때는 new 명령어를 사용해야함
  • DTO의 package이름을 다 적어줘야해서 지저분함
  • 생성자 방식만 지원함

Querydsl 빈 생성(Bean population)

결과를 DTO 반환할 때 사용
다음 3가지 방법 지원

  1. 프로퍼티 접근 - Setter
List<MemberDto> result = queryFactory
			.select(Projections.bean(MemberDto.class,
	 			member.username, member.age))
			.from(member)
 			.fetch();
  1. 필드 직접 접근
List<MemberDto> result = queryFactory
 		.select(Projections.fields(MemberDto.class,
        	member.username, member.age))
 		.from(member)
 		.fetch();

별칭이 다를 때

package study.querydsl.dto;
import lombok.Data;
@Data
public class UserDto {
 private String name;
 private int age;
}


List<UserDto> fetch = queryFactory
 	.select(Projections.fields(UserDto.class,
 	member.username.as("name"),
 		ExpressionUtils.as(
 			JPAExpressions
 				.select(memberSub.age.max())
 				.from(memberSub), "age")
 		)
 	).from(member)
 	.fetch();
  • 프로퍼티나, 필드 접근 생성 방식에서 이름이 다를 때 해결 방안
  • ExpressionUtils.as(source,alias) : 필드나, 서브 쿼리에 별칭 적용
  • username.as("memberName") : 필드에 별칭 적용
  1. 생성자 사용
List<MemberDto> result = queryFactory
 	.select(Projections.constructor(MemberDto.class,
		 member.username, member.age))
 	.from(member)
 	.fetch();
}
  • type을 보고 들어가기 때문에, Dtot에서의 이름이 달라도 맞춰 들어간다.

프로젝션과 결과 반환 - @QueryProjection

가장 깔금한 해결책이긴 한데 단점이 있다.

@QueryProjection
 public MemberDto(String username, int age) {
 this.username = username;
 this.age = age;
 }
---------------------------------------------------------
List<MemberDto> result = queryFactory
	.select(new QMemberDto(member.username, member.age))
 	.from(member)
	.fetch(); 
  • 이 방법은 컴파일러로 타입을 체크할 수 있으므로 가장 안전한 방법이다. 다만 DTO에 QueryDSL 애노테이션을 유지해야 하는 점(의존성 문제)과 DTO까지 Q 파일을 생성해야 하는 단점이 있다.

distinct

List<String> result = queryFactory
	.select(member.username).distinct()
 	.from(member)
 	.fetch();

동적 쿼리

Querlydsl로 동적쿼리를 작성하는 2가지 방법

  1. BooleanBuilder
List<Member> result = searchMember1(usernameParam, ageParam);


private List<Member> searchMember1(String usernameCond, Integer ageCond) {
 	BooleanBuilder builder = new BooleanBuilder();
    
 	if (usernameCond != null) {
 		builder.and(member.username.eq(usernameCond));
 	}
    
 	if (ageCond != null) {
 		builder.and(member.age.eq(ageCond));
 	}
    
 	return queryFactory
 			.selectFrom(member)
 			.where(builder)
 			.fetch();
}
  • null일시 조건이 안들어감. null이 아닐시 조건이 들어감.
  • BooleanBuilder()를 만들때 초기값을 줄 수 도 있음.
  1. Where 다중 파라미터
List<Member> result = searchMember2(usernameParam, ageParam);


private List<Member> searchMember2(String usernameCond, Integer ageCond) {
 	return queryFactory
 			.selectFrom(member)
 			.where(usernameEq(usernameCond), ageEq(ageCond))
 			.fetch();
}

private BooleanExpression usernameEq(String usernameCond) {
 	return usernameCond != null ? member.username.eq(usernameCond) : null;
}

private BooleanExpression ageEq(Integer ageCond) {
 	return ageCond != null ? member.age.eq(ageCond) : null;
}
  • where 조건에 null 값은 무시된다.
  • 메서드를 다른 쿼리에서도 재활용 할 수 있다.
  • 쿼리 자체의 가독성이 높아진다.
  • BooleanExpression을 사용했는데, Predicate도 사용 가능하다.
private Predicate ageEq(Integer ageCond) {
 	return ageCond != null ? member.age.eq(ageCond) : null;
}
  • 둘 중에 뭘 사용하냐면 BooleanExpression이 더 나은거같다. 조립을 하려면 BooleanExpression을 사용해야 되서.
private BooleanExpression allEq(String usernameCond, Integer ageCond) {
	return usernameEq(usernameCond).and(ageEq(ageCond));
}
  • null 체크는 주의해서 처리해야함

수정, 삭제 배치 쿼리

JPA의 dirty checking은 건건이 업데이트문이 날아간다. 이런 것을 벌크 연산으로 한번에 실행 가능.

쿼리 한번으로 대량 데이터 수정

long count = queryFactory
 		.update(member)
 		.set(member.username, "비회원")
 		.where(member.age.lt(28))
 		.execute();

기존 숫자에 1 더하기

long count = queryFactory
 		.update(member)
 		.set(member.age, member.age.add(1))
 		.execute();
  • 곱하기: .set(member.age, member.age.multiply(2)) 로 하면됨..
  • 나가는 쿼리 문
update member
 set age = age + 1

쿼리 한번으로 대량 데이터 삭제

long count = queryFactory
 .delete(member)
 .where(member.age.gt(18))
 .execute();

주의: JPQL 배치와 마찬가지로, 영속성 컨텍스트에 있는 엔티티를 무시하고 실행되기 때문에 배치 쿼리를 실행하고 나면 영속성 컨텍스트를 초기화 하는 것이 안전하다.

SQL Function 호출하기

SQL function은 JPA와 같이 Dialect에 등록된 내용만 호출할 수 있다

queryFactory
	.selectFrom(Expressions.stringTemplate(
    	"function('replace', {0}, {1}, {2})", member.username, "member","M"))
    .from(Mmember)
    .fetchFrist()

소문자로 변경해서 비교해라.

List<String> result = queryFactory
	.select(member.username)
	.from(member)
//	.where(member.username.eq(
//    	Expressions.stringTemplate("function('lower', {0})",member.username)))
    .where(member.username.eq(member.username.lower()))
    .fetch();

lower 같은 ANSI 표준 함수들은 querydsl이 상당부분 내장하고 있다. 따라서 다음과 같이 처리해도 결과는 같다.

순수 JPA 리포지토리

@Repository
public interface Repository {
	
    private final EntityManager em;
    private final JPAQueryFactory queryFactory;
    
    public Repository(EntityManager em) {
    	this.em = em;
        this.queryFactory = new JPAQueryFactory(em);
    }
}
  • Querydsl을 사용하려면 JPAQueryFacotry를 받아야됨. queryFacotry는 EntityManager를 넣어줘야됨.
  • @RequiredArgsConstructor는 사용못하긴 하지만, 직접 생성자로 넣어주면 됨.
  • 아예 밖에서 처리하고, 받아도 됨.
@Configuration
public class QueryDslConfig {

    @PersistenceContext
    public EntityManager em;

    @Bean
    public JPAQueryFactory queryFactory() {
        return new JPAQueryFactory(em);
    }
}


@Repository
@RequiredArgsConstructor
public interface Repository {
	
    private final JPAQueryFactory queryFactory;
    
    
}
  • Spring Bean으로 등록해버려서 싱글톤으로 그냥 사용하면 된다. 따로 EntitiyManager도 안넣어 줘도 됨.

참고: 동시성 문제는 걱정하지 않아도 된다. 왜냐하면 여기서 스프링이 주입해주는 엔티티 매니저는 실제 동작 시점에 진짜 엔티티 매니저를 찾아주는 프록시용 가짜 엔티티 매니저이다. 이 가짜 엔티티 매니저는 실제 사용 시점에 트랜잭션 단위로 실제 엔티티 매니저(영속성 컨텍스트)를 할당해준다.

  • StringUtils > SpringFramework에서 제공하는 utils class.. 조건 비교에서 hasText등을 많이 사용.

실무에서 동적 쿼리를 짤때는 기본 조건이라도 있는것이 좋다. 만약 조건 쿼리가 없어서 전부 가져온다면 순식간에 몇십만, 몇백만건의 데이터를 퍼올릴 수 있음. (큰일난다는 뜻)
따라서, limit라던가, 페이징이라던가 무슨 조건이라도 있는 것이 좋음.

public List<MemberTeamDto> search(MemberSearchCondition condition) {
	return queryFactory
    	.select(new QMemberTeamDto(
 			member.id,
 			member.username,
 			member.age,
 			team.id,
 			team.name))
 		.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) ? null : member.username.eq(username);
}
private BooleanExpression teamNameEq(String teamName) {
 return hasText(teamName) ? null : team.name.eq(teamName);
}
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; 
}

RestAPI로..

@GetMapping("/members")
public List<MemberTeamDto> searchMemberV1(MemberSearchCondition condition) {
	return memberJpaRepository.search(condition);
}
  • /members로 조회하면 JSON Data가 잘 넘어 온다.
  • 여기서 필터 조건으로 username, teamName, ageGoe, ageLoe를 쿼리 파라미터로 넘기면 그 조건에 맞춰 필터를 해준다.

스프링 데이터 JPA와 Query Dsl

public interface MemberRepository extends JpaRepository<Member, Long> {
	List<Member> findByUsername(String username);
}
  • Querydsl 전용 기능인 회원 search를 작성할 수 없다. 사용자 정의 리포지토리 필요

사용자 정의 리포지토리 만드는 법

public interface MemberRepositoryCustom {
	List<MemberTeamDto> search(MemberSearchCondition condition);
}


public class MemberRepositoryImpl implements MemberRepositoryCustom 

	private final JPAQueryFactory queryFactory;
    
    @Override
    List<MemberTeamDto> search(MemberSearchCondition condition) {
    	...
    }

}
  • 구현부분은 기존의 JPA 로직과 똑같기 때문에 구현은 하지 않겠다.
  • 사용자 정의 리포지토리는 기존 Spring Data JPA를 구현한 Repository의 이름에 +Impl을 붙여 주어야됨. (이게 규칙임. 마음대로 이름 지으면 절대 안됨, 커스텀 인터페이스는 이름 상관 없음)

스프링 데이터 페이징 + QueryDsl

@Override
public Page<MemberTeamDto> searchPageComplex(MemberSearchCondition condition, Pageable pageable) {
	List<MemberTeamDto> content = queryFactory
							.select(new QMemberTeamDto(
									member.id,
									member.username,
									member.age,
									team.id,
									team.name))
							.from(member)
							.leftJoin(member.team, team)
							.where(usernameEq(condition.getUsername()),
                            				  teamNameEq(condition.getTeamName()),
											  ageGoe(condition.getAgeGoe()),
											  ageLoe(condition.getAgeLoe()))
							.offset(pageable.getOffset())
							.limit(pageable.getPageSize())
							.fetch();
                            
	long total = queryFactory
    		.select(member)
			.from(member)
			.leftJoin(member.team, team)
			.where(usernameEq(condition.getUsername()),
				   teamNameEq(condition.getTeamName()),
				   ageGoe(condition.getAgeGoe()),
				   ageLoe(condition.getAgeLoe()))
			.fetchCount();
            
//return new PageImpl<>(content, pageable, total); 밑의 방식으로 리턴

return PageableExecutionUtils.getPage(content, pageable, countQuery::fetchCount); 
  • pageable을 값으로 받으면 됨. 위의 쿼리는 totalCount와 페이징 쿼리를 분리해서 사용함.
  • 예전에는 fetchResult()로 카운트 쿼리와 조회 쿼리를 한번에 사용가능했지만, 문제가 있어서 deprecation 됨.
  • 리턴 타입으로 PageableExecutionUtils를 사용하면 좀 더 쿼리가 최적화되 나감.

QuerydslPredicateExecutor

QuerydslPredicateExecutor 인터페이스

public interface QuerydslPredicateExecutor<T> {
	Optional<T> findById(Predicate predicate);
	Iterable<T> findAll(Predicate predicate);
	long count(Predicate predicate);
	boolean exists(Predicate predicate);
	// … more functionality omitted.
}

리포지토리에 적용
interface MemberRepository extends JpaRepository<User, Long>, QuerydslPredicateExecutor<User> {
}

테스트 
Iterable result = memberRepository.findAll(
	member.age.between(10, 40).and(member.username.eq("member1"))
);

한계점

  • 조인X (묵시적 조인은 가능하지만 left join이 불가능하다.)
  • 클라이언트가 Querydsl에 의존해야 한다. 서비스 클래스가 Querydsl이라는 구현 기술에 의존해야 한다.
  • 복잡한 실무환경에서 사용하기에는 한계가 명확하다
profile
공부 정리 블로그

0개의 댓글