중급자를 위해 준비한
[백엔드, 웹 개발] 강의입니다.
Querydsl의 기초부터 실무 활용까지, 한번에 해결해보세요!
✍️
이런 걸
배워요!
Querydsl을 기초부터 실무활용까지 한번에 배울 수 있습니다.
단순한 기능 설명을 넘어 실무활용 노하우를 배울 수 있습니다.
JPA를 사용할 때 동적 쿼리와 복잡한 쿼리 문제를 해결할 수 있습니다.
복잡한 쿼리, 동적 쿼리는 이제 안녕!
Querydsl로 자바 백엔드 기술을 단단하게.
???? 본 강의는 로드맵 과정입니다.
- 본 강의는 자바 백엔드 개발의 실전 코스를 완성하는 마지막 강의입니다. 스프링 부트와 JPA 실무 완전 정복 로드맵을 우선 확인해주세요. (링크)
강좌
https://www.inflearn.com/course/querydsl-실전#
강의자료
https://github.com/braverokmc79/jpa-basic-lecture-file2
소스 :
https://github.com/braverokmc79/jpa-querydsl
[8] 스프링 데이터 JPA가 제공하는 Querydsl 기능(실무 환경에서 사용하기에는 많이 부족)
참고삼아 보고, 42. Querydsl 지원 클래스 직접 만들기 를 볼것
여기서 소개하는 기능은 제약이 커서 복잡한 실무 환경에서 사용하기에는 많이 부족하다.
그래도 스프링 데이터에서 제공하는 기능이므로 간단히 소개하고, 왜 부족한지 설명하겠다.
39.인터페이스 지원 - QuerydslPredicateExecutor
강의 :
https://www.inflearn.com/course/lecture?courseSlug=querydsl-실전&unitId=30155&tab=curriculum
인터페이스 지원 - QuerydslPredicateExecutor 공식 URL: https://docs.spring.io/spring-data/jpa/docs/2.2.3.RELEASE/reference/html/ #core.extensions.querydsl
package study.querydsl.repository; import org.springframework.data.jpa.repository.JpaRepository; import org.springframework.data.querydsl.QuerydslPredicateExecutor; import study.querydsl.entity.Member; import java.util.List; public interface MemberRepository extends JpaRepository<Member, Long> , MemberRepositoryCustom, QuerydslPredicateExecutor<Member> { List<Member> findByUsername(String username); }
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이라는 구현 기술에 의존해야 한다.
복잡한 실무환경에서 사용하기에는 한계가 명확하다.
> 참고: QuerydslPredicateExecutor 는 Pagable, Sort를 모두 지원하고 정상 동작한다.
40. Querydsl Web 지원
강의 :
https://www.inflearn.com/course/lecture?courseSlug=querydsl-실전&unitId=30156&tab=curriculum
공식 URL: https://docs.spring.io/spring-data/jpa/docs/2.2.3.RELEASE/reference/html/
#core.web.type-safe
한계점
단순한 조건만 가능
조건을 커스텀하는 기능이 복잡하고 명시적이지 않음
컨트롤러가 Querydsl에 의존
복잡한 실무환경에서 사용하기에는 한계가 명확
41. 리포지토리 지원 - QuerydslRepositorySupport
강의 :
https://www.inflearn.com/course/lecture?courseSlug=querydsl-실전&unitId=30157&tab=curriculum
장점
getQuerydsl().applyPagination() 스프링 데이터가 제공하는 페이징을 Querydsl로 편리하게 변환 가능(단! Sort는 오류발생)
from() 으로 시작 가능(최근에는 QueryFactory를 사용해서 select() 로 시작하는 것이 더 명시적) EntityManager 제공
한계
Querydsl 3.x 버전을 대상으로 만듬
Querydsl 4.x에 나온 JPAQueryFactory로 시작할 수 없음 select로 시작할 수 없음 (from으로 시작해야함)
QueryFactory 를 제공하지 않음 스프링 데이터 Sort 기능이 정상 동작하지 않음
42. Querydsl 지원 클래스 직접 만들기
강의 :
https://www.inflearn.com/course/lecture?courseSlug=querydsl-실전&unitId=30158&tab=curriculum
장점
스프링 데이터가 제공하는 페이징을 편리하게 변환
페이징과 카운트 쿼리 분리 가능
스프링 데이터 Sort 지원
select() , selectFrom() 으로 시작 가능
EntityManager , QueryFactory 제공
Querydsl4RepositorySupport
package study.querydsl.repository.support; import com.querydsl.core.types.EntityPath; import com.querydsl.core.types.Expression; import com.querydsl.core.types.dsl.PathBuilder; import com.querydsl.jpa.impl.JPAQuery; import com.querydsl.jpa.impl.JPAQueryFactory; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.data.domain.Page; import org.springframework.data.domain.Pageable; import org.springframework.data.jpa.repository.support.JpaEntityInformation; import org.springframework.data.jpa.repository.support.JpaEntityInformationSupport; import org.springframework.data.jpa.repository.support.Querydsl; import org.springframework.data.querydsl.SimpleEntityPathResolver; import org.springframework.data.repository.support.PageableExecutionUtils; import org.springframework.stereotype.Repository; import org.springframework.util.Assert; import javax.annotation.PostConstruct; import javax.persistence.EntityManager; import java.util.List; import java.util.function.Function; /** * Querydsl 4.x 버전에 맞춘 Querydsl 지원 라이브러리 * * @author Younghan Kim * @see org.springframework.data.jpa.repository.support.QuerydslRepositorySupport */ @Repository public abstract class Querydsl4RepositorySupport { private final Class domainClass; private Querydsl querydsl; private EntityManager entityManager; private JPAQueryFactory queryFactory; public Querydsl4RepositorySupport(Class<?> domainClass) { Assert.notNull(domainClass, "Domain class must not be null!"); this.domainClass = domainClass; } @Autowired public void setEntityManager(EntityManager entityManager) { Assert.notNull(entityManager, "EntityManager must not be null!"); JpaEntityInformation entityInformation = JpaEntityInformationSupport.getEntityInformation(domainClass, entityManager); SimpleEntityPathResolver resolver = SimpleEntityPathResolver.INSTANCE; EntityPath path = resolver.createPath(entityInformation.getJavaType()); this.entityManager = entityManager; this.querydsl = new Querydsl(entityManager, new PathBuilder<>(path.getType(), path.getMetadata())); this.queryFactory = new JPAQueryFactory(entityManager); } @PostConstruct public void validate() { Assert.notNull(entityManager, "EntityManager must not be null!"); Assert.notNull(querydsl, "Querydsl must not be null!"); Assert.notNull(queryFactory, "QueryFactory must not be null!"); } protected JPAQueryFactory getQueryFactory() { return queryFactory; } protected Querydsl getQuerydsl() { return querydsl; } protected EntityManager getEntityManager() { return entityManager; } protected <T> JPAQuery<T> select(Expression<T> expr) { return getQueryFactory().select(expr); } protected <T> JPAQuery<T> selectFrom(EntityPath<T> from) { return getQueryFactory().selectFrom(from); } protected <T> Page<T> applyPagination(Pageable pageable, Function<JPAQueryFactory, JPAQuery> contentQuery) { JPAQuery jpaQuery = contentQuery.apply(getQueryFactory()); List<T> content = getQuerydsl().applyPagination(pageable, jpaQuery).fetch(); return PageableExecutionUtils.getPage(content, pageable, jpaQuery::fetchCount); } protected <T> Page<T> applyPagination(Pageable pageable, Function<JPAQueryFactory, JPAQuery> contentQuery, Function<JPAQueryFactory, JPAQuery> countQuery) { JPAQuery jpaContentQuery = contentQuery.apply(getQueryFactory()); List<T> content = getQuerydsl().applyPagination(pageable, jpaContentQuery).fetch(); JPAQuery countResult = countQuery.apply(getQueryFactory()); return PageableExecutionUtils.getPage(content, pageable, countResult::fetchCount); } }
Querydsl4RepositorySupport 사용 코드
package study.querydsl.repository; import com.querydsl.core.types.dsl.BooleanExpression; import com.querydsl.jpa.impl.JPAQuery; import org.springframework.data.domain.Page; import org.springframework.data.domain.Pageable; import org.springframework.data.support.PageableExecutionUtils; import org.springframework.stereotype.Repository; import study.querydsl.dto.MemberSearchCondition; import study.querydsl.dto.MemberTeamDto; import study.querydsl.dto.QMemberTeamDto; import study.querydsl.entity.Member; import study.querydsl.entity.QMember; import study.querydsl.repository.support.Querydsl4RepositorySupport; import java.util.List; import static org.springframework.util.StringUtils.hasText; import static study.querydsl.entity.QMember.member; import static study.querydsl.entity.QTeam.team; @Repository public class MemberTestRepository extends Querydsl4RepositorySupport { public MemberTestRepository(Class<?> domainClass) { super(domainClass); } public List<Member> basicSelect(){ return select(member) .from(member) .fetch(); } public List<Member> basicSelectFrom(){ return selectFrom(member).fetch(); } public Page<Member> searchPageByApplyPage(MemberSearchCondition condition, Pageable pageable){ JPAQuery<Member> query = selectFrom(member) .where(usernameEq(condition.getUsername()), teamNameEq(condition.getTeamName()), ageGoe(condition.getAgeGoe()), ageLoe(condition.getAgeLoe()) ); List<Member> content = getQuerydsl().applyPagination(pageable, query).fetch(); return PageableExecutionUtils.getPage(content, pageable, query::fetchCount); } /** searchPageByApplyPage 메서드와 applyPagination 는 반환 값은 동일하다 */ public Page<Member> applyPagination(MemberSearchCondition condition, Pageable pageable){ return applyPagination(pageable, query->query .selectFrom(member) .leftJoin(member.team, team) .where(usernameEq(condition.getUsername()), teamNameEq(condition.getTeamName()), ageGoe(condition.getAgeGoe()), ageLoe(condition.getAgeLoe()) )); } /** applyPagination2 메서드와 MemberRepositoryImpl 클래스의 searchPageComplex 는 반환 값은 동일하다 */ public Page<Member> applyPagination2(MemberSearchCondition condition, Pageable pageable){ return applyPagination(pageable, contentQuery->contentQuery .selectFrom(member) .leftJoin(member.team, team) .where(usernameEq(condition.getUsername()), teamNameEq(condition.getTeamName()), ageGoe(condition.getAgeGoe()), ageLoe(condition.getAgeLoe()) ), countQuery -> countQuery .selectFrom(member) .leftJoin(member.team, team) .where(usernameEq(condition.getUsername()), teamNameEq(condition.getTeamName()), ageGoe(condition.getAgeGoe()), ageLoe(condition.getAgeLoe())) ); } /** * 복잡한 페이징 * 데이터 조회 쿼리와, 전체 카운트 쿼리를 분리 */ /** @Override public Page<MemberTeamDto> searchPageComplex(MemberSearchCondition condition, Pageable pageable) { List<MemberTeamDto> content = 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()), ageGoeEq(condition.getAgeGoe()), ageLoeEq(condition.getAgeLoe()) ) .offset(pageable.getOffset()) .limit(pageable.getPageSize()) .fetch(); JPAQuery<Member> countQuery = queryFactory .selectFrom(member) // .leftJoin(member.team, team) .where( usernameEq(condition.getUsername()), teamNameEq(condition.getTeamName()), ageGoeEq(condition.getAgeGoe()), ageLoeEq(condition.getAgeLoe()) ); // .fetchCount(); //첫페이지가 페이지 사이즈 content 사이즈 보다 작을 경우 countQuery.fetchCount() 작동하지 않고 해당 content 사이즈로 처리 //마지막 페이지일경우 countQuery 실행되지 않고, offset + 컨텐츠 사이즈를 더해서 전체 사이즈 구함 // return PageableExecutionUtils.getPage(content,pageable , ()->countQuery.fetchCount() ); return PageableExecutionUtils.getPage(content,pageable , countQuery::fetchCount ); //return new PageImpl<>(content, pageable, total); } /** * count 쿼리가 생략 가능한 경우 생략해서 처리 * 페이지 시작이면서 컨텐츠 사이즈가 페이지 사이즈보다 작을 때 * 마지막 페이지 일 때 (offset + 컨텐츠 사이즈를 더해서 전체 사이즈 구함) */ 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; } }
★4.3 스프링 부트 2.6 이상, Querydsl 5.0 지원 방법
참고: 해당 내용은 강의 이후에 추가된 내용입니다.
최신 스프링 부트 2.6부터는 Querydsl 5.0을 사용한다.
스프링 부트 2.6 이상 사용시 다음과 같은 부분을 확인해야 한다.
1. build.gradle 설정 변경
2. PageableExecutionUtils Deprecated(향후 미지원) 패키지 변경
3. Querydsl fetchResults() , fetchCount() Deprecated(향후 미지원)
build.gradle 설정 방법
//querydsl 추가 buildscript { ext { queryDslVersion = "5.0.0" } } plugins { id 'java' id 'org.springframework.boot' version '2.7.9' id 'io.spring.dependency-management' version '1.0.15.RELEASE' //querydsl 추가 id "com.ewerk.gradle.plugins.querydsl" version "1.0.10" } group = 'study' version = '0.0.1-SNAPSHOT' sourceCompatibility = '17' configurations { compileOnly { extendsFrom annotationProcessor } } repositories { mavenCentral() } dependencies { implementation 'org.springframework.boot:spring-boot-starter-data-jpa' implementation 'org.springframework.boot:spring-boot-starter-web' implementation 'com.github.gavlyukovskiy:p6spy-spring-boot-starter:1.5.8' //querydsl 추가 implementation "com.querydsl:querydsl-jpa:${queryDslVersion}" implementation "com.querydsl:querydsl-apt:${queryDslVersion}" compileOnly 'org.projectlombok:lombok' developmentOnly 'org.springframework.boot:spring-boot-devtools' runtimeOnly 'com.h2database:h2' runtimeOnly 'com.mysql:mysql-connector-j' annotationProcessor 'org.projectlombok:lombok' testImplementation 'org.springframework.boot:spring-boot-starter-test' } tasks.named('test') { useJUnitPlatform() } //querydsl 추가 시작 def querydslDir = "$buildDir/generated/querydsl" querydsl { jpa = true querydslSourcesDir = querydslDir } sourceSets { main.java.srcDir querydslDir } compileQuerydsl{ options.annotationProcessorPath = configurations.querydsl } configurations { compileOnly { extendsFrom annotationProcessor } querydsl.extendsFrom compileClasspath } //querydsl 추가 끝
querydsl-jpa , querydsl-apt 를 추가하고 버전을 명시해야 한다.
참고로 위 설정은 JUnit은 4를 사용한다.
JUnit 5를 사용하려면 다음 설정을 제거하면 된다.
exclude group: 'org.junit.vintage', module: 'junit-vintage-engine'
PageableExecutionUtils Deprecated(향후 미지원) 패키지 변경
PageableExecutionUtils 클래스 사용 패키지 변경
기능이 Deprecated 된 것은 아니고, 사용 패키지 위치가 변경되었습니다. 기존 위치를 신규 위치로 변경해주시면 문제 없이 사용할 수 있습니다.
기존: org.springframework.data.repository.support.PageableExecutionUtils
신규: org.springframework.data.support.PageableExecutionUtils
★ ★ Querydsl fetchResults() , fetchCount() Deprecated(향후 미지원)
Querydsl의 fetchCount() , fetchResult() 는 개발자가 작성한 select 쿼리를 기반으로 count용 쿼리를 내부에서 만들어서 실행합니다.
그런데 이 기능은 강의에서 설명드린 것 처럼 select 구문을 단순히 count 처리하는 용도로 바꾸는 정도입니다.
따라서 단순한 쿼리에서는 잘 동작하지만, 복잡한 쿼리에서는 제대로 동작하지 않습니다.
Querydsl은 향후 fetchCount() , fetchResult() 를 지원하지 않기로 결정했습니다.
참고로 Querydsl의 변화가 빠르지는 않기 때문에 당장 해당 기능을 제거하지는 않을 것입니다. 따라서 count 쿼리가 필요하면 다음과 같이 별도로 작성해야 합니다.
★ count 쿼리는 예제
@Test public void count() { Long totalCount = queryFactory //.select(Wildcard.count) //select count(*) .select(member.count()) //select count(member.id) .from(member) .fetchOne(); System.out.println("totalCount = " + totalCount); }
count(*) 을 사용하고 싶으면 예제의 주석처럼 Wildcard.count 를 사용하시면 됩니다.
member.count() 를 사용하면 count(member.id) 로 처리됩니다.
응답 결과는 숫자 하나이므로 fetchOne() 을 사용합니다.
MemberRepositoryImpl.searchPageComplex() 예제에서 보여드린 것 처럼 select 쿼리와는 별도로 count 쿼리를 작성하고 fetch() 를 사용해야 합니다.
다음은 최신 버전에 맞추어 수정된 예제입니다.
★수정된 searchPageComplex 예제★
import org.springframework.data.support.PageableExecutionUtils; //패키지 변경 public Page < MemberTeamDto > searchPageComplex(MemberSearchCondition condition, Pageable pageable) { List <MemberTeamDto> content = 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()) ) .offset(pageable.getOffset()) .limit(pageable.getPageSize()) .fetch(); JPAQuery <Long> countQuery = queryFactory .select(member.count()) .from(member) .leftJoin(member.team, team) .where( usernameEq(condition.getUsername()), teamNameEq(condition.getTeamName()), ageGoe(condition.getAgeGoe()), ageLoe(condition.getAgeLoe()) ); return PageableExecutionUtils.getPage(content, pageable, countQuery::fetchOne); }
댓글 ( 4)
댓글 남기기