QueryDSL

박영준·2023년 3월 23일
1

JPA

목록 보기
2/8

1. 정의

  • 정적 타입을 이용해서, 쿼리(SQL 같은...)를 생성할 수 있도록 해주는 프레임워크

  • SQL, JPQL을 코드로 작성할 수 있도록 도와주는 빌더 API

2. QueryDSL 필요성

1) JPQL 를 보완

JPQL 사용 이유
Spring Data JPA 가 기본적으로 제공해주는 CRUD 메서드 및 쿼리 메서드 기능 + 원하는 조건의 데이터를 수집하기 위해

JPQL 문제점
간단한 로직을 작성하는데 큰 문제는 없으나
복잡한 로직의 경우, 개행이 포함된 쿼리 문자열이 매우 길어진다.
→ 이때 JPQL 문자열에 오타/문법적인 오류가 있다면, 정적 쿼리라면 어플리케이션 로딩 시점에 발견 가능하나 그 외는 런타임 시점에서 에러가 발생
→ 이러한 문제를 어느 정도 해소하는데 기여하는 프레임워크가 QueryDSL

2) 직접적인 연관관계를 맺지 않는 Entity의 Join이 어렵다

(Entity A, B, C 사이의 연관 관계가 위 도식과 같이 A -(One to Many)→ B -(One to Many)→ C 로 설정)

  • 이때, A Repository 를 사용하여 특정 조건을 만족하는 C 를 가진 A 를 검색할 수 없다.

  • Repository 를 통해 검색하지 않고, A 내부의 B 리스트를 순회 + 다시 B 내부의 C 리스트를 순회하여 찾을 경우
    수 많은 단계의 반복문, select 쿼리, 비교연산이 발생할 수 있으며,
    최악의 경우 연관된 모든 Entity가 메모리에 로드 될 수 있다.

3) Entity들 中 원하는 필드만 가져올 수 없다.

DTO를 통해 '리소스 A의 정보 + 연관된 리소스 B 의 정보 중 일부'를 함께 반환하고 싶은 경우,
A Repository 와 B Repository 를 통해 Entity A 와 Entity B 를 따로따로 반환받은 후 A ResonseDto 로 생성해야 한다.

이 때, Entity A 와 B 의 모든 필드들의 데이터를 가져오게 된다.

기존의 SQL을 직접 작성하는 경우 : 직접 select문에 명시하여 필요한 데이터만을 가져올 수 있다
JPAReposirtory 를 사용하는 경우 : Entity의 멤버의 종류 多 / Join 대상이 多 경우, 필요한 데이터에 비해 너무 多 데이터를 읽어오게 된다.

3. 비교

1) 복잡한 쿼리

JPQL

@Test
@DisplayName("댓글 조회 테스트_JPQL")
void read_replies_test() throws Exception {
    //given
    String username = "name";
    String title = "title";
    String content = "content";
    //when
    String query = "select r "
        + "from Reply r "
        + "left join fetch r.board b "
        + "left join fetch r.user u "
        + "left join fetch r.parent p "
        + "where ( "
        + "b.title = :title "
        + "and u.name = :username "
        + "and r.content = :content "
        + " ) "
        + "order by r.id desc";
        
    List<Reply> replies = em.createQuery(query, Reply.class)
        .setParameter("username", username)
        .setParameter("title", title)
        .setParameter("content", content)
        .getResultList();
    //then
}
  • 복잡한 쿼리를 타이핑 中 오타 발생 위험

  • 가독성 ↓

  • 문자열로 쿼리를 짜므로, 컴파일 시점에 디버깅 & Type-check 가 불가능하다.
    → SQL 의 문제점이기도 함
    (단, 일부 JPQL의 에러는 IDE의 도움을 받을 수 있다.)

QueryDSL

@Test
@DisplayName("댓글 조회 테스트_Querydsl")
void read_replies_test_querydsl() throws Exception {
    //given
    String username = "name";
    String title = "title";
    String content = "content";
    //when
    QReply parent = new QReply("p");
    List<Reply> replies = factory
    	// select :  SQL을 통해 어떤 데이터를 가져올지 지정. Entity의 모든 데이터를 읽기 or 클래스 접근자를 통해 Entity 내의 일부 정보만 가져오도록 할 수 있다.
        // from : 어떤 테이블에서 데이터를 조회할 지 지정. Entity를 파라메터로 전달하여 조회할 테이블을 지정할 수 있다. 서브 쿼리를 지원하지 X
        .selectFrom(reply)		
        .leftJoin(reply.board, board).fetchJoin()
        .leftJoin(reply.user, user).fetchJoin()
        .leftJoin(reply.parent, parent).fetchJoin()
        // where : 어떠한 조건으로 검색할지 지정(다양한 조건을 지원함). 파라미터를 여러 개 전달할 경우, 자동으로 AND 연산을 적용. OR 연산은 선행한 비교조건에 체이닝하여 적용할 수 있다. 서브 쿼리를 지원 O
        .where(
            user.name.eq(username),
            board.title.eq(title),
            reply.content.eq(content)
        )
        .orderBy(reply.id.desc())
        // fetch / execute : fetchOne(), fetch()와 같이 하나의 레코드만 가져올지, 어려개의 레코드를 가져올지 지정하여 쿼리를 실행할 수 있다. 조회 쿼리가 아닌경우 execute() 호출하여 실행
        .fetch();
    //then
}
  • java 코드를 사용하므로(문자가 아님), 컴파일 시점에 IDE의 도움으로 문법적 오류(타입 체크, 오타...) 잡기 & 코드 자동완성이 가능하다.
    → JPQL 은 정적 쿼리이므로, 문자열을 더해야해서 복잡하다.
    반면, QueryDSL 은 동적 쿼리로써, 코드를 더하는 것이어서 보다 수월하다.
  • 파라미터를 바인딩 할 때, 바로 꽂아 넣을 수 있다.
    → 간편, 가독성 ↑

2) 동적 쿼리

JPQL

  • Criteria 를 이용해서 동적 쿼리를 사용하는데, 이것은 매우 복잡하다.

QueryDSL

@Test
@DisplayName("동적 쿼리")
void dynamicQuery_test() throws Exception {
    //given
    String username = "name";
    String title = "title";
    String content = "content";
    //when
    QReply parent = new QReply("p");
    List<Reply> replies = factory
        .selectFrom(reply)
        .leftJoin(reply.board, board).fetchJoin()
        .leftJoin(reply.user, user).fetchJoin()
        .leftJoin(reply.parent, parent).fetchJoin()
        .where(
            dynamicSearch(username, title, content)
        )
        .orderBy(reply.id.desc())
        .fetch();
    //then
}

private BooleanBuilder dynamicSearch(String username, String title, String content) {
    BooleanBuilder builder = new BooleanBuilder();
    if(username != null){
        builder.and(user.name.eq(username));
    }
    if(title != null){
        builder.and(board.title.eq(title));
    }
    if(content != null){
        builder.and(reply.content.eq(content));
    }
    return builder;
}
  • BooleanBuilder나 다중 파라미터 등...을 이용해 동적 쿼리를 손쉽게 사용할 수 있다.
    → BooleanBuilder 에 조건을 넣고 쿼리를 실행시키면, 쉽게 동적 쿼리가 생성된다.

  • 쉬운 SQL 스타일의 문법으로 쉽게 학습할 수 있다.

  • 원하는 필드만 뽑아서 DTO로 뽑아내는 기능도 QueryDSL 이 지원한다.

3) 시점

JPQL, SQL

  • 컴파일 시점에 알 수 없고, 잘 해봐야 애플리케이션 로딩 시점에 알 수 있다.
    → 자바와 문자열의 한계

  • 해당 로직 실행 전까지 작동여부 확인 X

  • 해당 쿼리 실행 시점에 오류를 발견

4) @Query 어노테이션

장점
- JPQL 이 아닌 Native 쿼리를 사용할 수 있다.
(Native 쿼리 : DB에 수행되는 SQL문을 직접 작성하므로, 특정 DB에서만 제공하는 기능이나 show tables 같은 JPA에서 기본적으로 지원하지 않는 기능을 사용할 때 사용 가능)

단점
- SQL문을 문자열 형태로 작성하므로, 컴파일 시점에 문법 에러를 발견하기 어렵다. (어플리케이션 구동 시점에 발견할 수 있다.)

5) JPA Criteria Query

장점
- 문법 오류를 컴파일 단계에서 발견할 수 있다.

단점
- @Query 어노테이션이나 Query DSL에 비하여, 사용방법이 복잡하여 실행하고자 하는 JPQL을 직관적으로 알기 어렵다.

3. JPAQueryFactory

class SampleQuerydslRepository {

    @PersistenceContext
    private EntityManager em;
    
    private JPAQueryFactory factory;
    
    ...
    
}
  • Querydsl 을 사용하기 위해서는 JPAQueryFactory 를 사용해야한다.

  • 각 메서드에서 생성해 사용하는 것보다는 Class의 필드로 선언해놓고 쓰는 것이 권장된다.

  • 멀티 스레드환경에서도 동시성 문제가 발생하지 않게 구현되어 있다.

  • Q-Type 을 사용한다. (static import 해놓고 사용하면 편함)

    import gorany.dslshop.entity.QBoard;
    import gorany.dslshop.entity.QReply;
    import gorany.dslshop.entity.QUser;
    ...

4. 설정

1) Entity 준비하기

2) build.gradle

buildscript {
    ext {
        queryDslVersion = "4.4.0"
    }
}

plugins {
    id 'org.springframework.boot' version '2.5.3'
    id 'io.spring.dependency-management' version '1.0.11.RELEASE'
    id 'java'
}

group = 'com.learning'
version = '0.0.1-SNAPSHOT'
sourceCompatibility = '1.8'

repositories {
    mavenCentral()
}

dependencies {
    implementation 'org.springframework.boot:spring-boot-starter-data-jpa'
    implementation 'org.springframework.boot:spring-boot-starter-web'
    runtimeOnly 'com.h2database:h2'
    testImplementation 'org.springframework.boot:spring-boot-starter-test'

    // QueryDSL
    	/* querydsl-apt 가 @Entity 및 @Id 등...의 어노테이션을 알 수 있도록, 
           annotationProcessor에 javax.persistence과 javax.annotation을 함께 추가 */
    implementation "com.querydsl:querydsl-jpa:${queryDslVersion}"
    
    // annotationProcessor : Java 컴파일러 플러그인. 컴파일 단계에서 어노테이션을 분석 및 처리함으로써 추가적인 파일을 생성 가능
    annotationProcessor(
            "javax.persistence:javax.persistence-api",
            "javax.annotation:javax.annotation-api",
            "com.querydsl:querydsl-apt:${queryDslVersion}:jpa")
}

// QueryDSL
sourceSets {
    main {
        java {
        	// generated 디렉토리 : 개발 환경에서 생성된(IDE의 개발 코드에서 생성) Q 클래스를 사용할 수 있도록 sourceSet 에 추가
        	// Gradle 설정을 통해, $projectDir/src/main/java의 프로덕션 코드에서 Q 클래스를 import해서 사용
            srcDirs = ["$projectDir/src/main/java", "$projectDir/build/generated"]
        }
    }
}

test {
    useJUnitPlatform()
}

3) Q 클래스

  1. Java 파일을 컴파일

  2. $projectDir/build/generated 디렉토리 하위에 Entity로 등록한 클래스들이 Q라는 접두사가 붙은 형태로 생성됐다.
    → 이런 클래스들을 Q 클래스 혹은 Q(쿼리) 타입 이라고 칭함
    → QueryDSL 로 쿼리 작성 시, Q 클래스를 사용함으로써 쿼리를 Type-Safe하게 작성 가능함

5. 예시

1) 기능 요구사항

  1. 본 서비스는 사용자의 입맛대로 "필터링"을 할 수 있는 기능이 있다.
  2. 필터링 기능은 "모임 방"에 적용할 수 있다.
  3. 필터링이 가능한 항목은 다음과 같다.
    • 제목 필터
    • 시간 필터
    • 종목 필터
    • 인원 필터
    • 지역 필터
    • 시간순
    • 인원순
    • 최신 등록순
    • 시간이 지난 방 보기 & 안보기
    • 인원이 마감된 방 보기 & 안보기
  4. 항목 내용은 페이징이 되어 응답한다.

2) Api

다음과 같은 동적인 값들은 QueryDsl 을 이용하여 처리한다.

  • URI : "/api/room"
  • Method : GET
  • Parameter :
    • roomTitle : 방 제목
    • roomContent : 방 내용
    • area(Array) : 지역
    • exercise(Array) : 운동 종목
    • startAppointmentDate : 운동 시작 시간
    • endAppointmentDate : 운동 끝 시간
    • containNoAdmittance(Boolean) : 인원 마감 보기 & 안보기
    • requiredPeopleCount : 모집 중이 인원
    • page : 페이지
    • size : 페이지 당 데이터 사이즈
    • sort(DESC, ASC) :
      • updatedTime : 최신 등록 순
      • start : 시간이 임박한 순
      • participant : 인원 순

3) QueryDsl 사용 X

public interface RoomRepository extends JpaRepository<Room, Long>, RoomRepositoryCustom {

    List<Room> findAllByContent(String Content);
    List<Room> findAllByContentAndStartAppointmentDateAndEndAppointmentDate(String Content, LocalDateTime startAppointmentDate, LocalDateTime endAppointmentDate);
    List<Room> findAllByContentAndParticipantCount(String content, int participantCount);

    ...

}
  • 쿼리 받아야할 개수가 11개 → Repository 에 추가될 메서드 多

  • 메서드 명이 너무 길다.

  • 파라미터가 너무 많다.

  • 단순 조회가 아닌, 추가적인 다른 조건절을 추가할 시, 더욱 복잡해진다.

3) QueryDsl 사용 O

(1) Gradle

buildscript {
	ext {
		queryDslVersion = "5.0.0"
	}
}

// 1. 추가
plugins {
	id 'org.springframework.boot' version '2.6.3'
	id 'io.spring.dependency-management' version '1.0.11.RELEASE'
	id 'java'
	id "com.ewerk.gradle.plugins.querydsl" version "1.0.10"
}

group = 'com.togethersports'
version = '0.0.1-SNAPSHOT'
sourceCompatibility = '11'

configurations {
	compileOnly {
		extendsFrom annotationProcessor
	}
}

repositories {
	mavenCentral()
}

// 2. 추가
dependencies {
	// spring dependencies
	implementation 'org.springframework.boot:spring-boot-starter-web'
	implementation 'org.springframework.boot:spring-boot-starter-data-jpa'
	implementation 'org.springframework.boot:spring-boot-starter-security'
	implementation 'org.springframework.boot:spring-boot-starter-oauth2-client'
	implementation 'org.springframework.security:spring-security-acl'
	implementation 'org.springframework.boot:spring-boot-starter-aop'
	implementation 'org.springframework.boot:spring-boot-starter-cache'
	implementation 'org.springframework.boot:spring-boot-starter-validation'

	// querydsl
	implementation "com.querydsl:querydsl-jpa:${queryDslVersion}"
	implementation "com.querydsl:querydsl-apt:${queryDslVersion}"

	annotationProcessor "org.springframework.boot:spring-boot-configuration-processor"

	// lombok & jdbc driver
	compileOnly 'org.projectlombok:lombok'
	runtimeOnly 'com.h2database:h2' // 개발용
	runtimeOnly 'mysql:mysql-connector-java' // 배포용
	annotationProcessor 'org.projectlombok:lombok'

	//websocket
	implementation 'org.springframework.boot:spring-boot-starter-websocket'
	implementation 'org.webjars:sockjs-client:1.1.2'
	implementation 'org.webjars:stomp-websocket:2.3.3-1'

	// utility
	implementation 'org.modelmapper:modelmapper:3.1.0'

	//  jwt
	implementation 'io.jsonwebtoken:jjwt-api:0.11.2'
	runtimeOnly 'io.jsonwebtoken:jjwt-impl:0.11.2'
	runtimeOnly'io.jsonwebtoken:jjwt-jackson:0.11.2'

	// poi (엑셀 파일 처리용 라이브러리)
	implementation 'org.apache.poi:poi-ooxml:5.2.2'

	// test
	testImplementation 'org.springframework.boot:spring-boot-starter-test'
	testImplementation 'org.springframework.security:spring-security-test'
}


tasks.named('test') {
	useJUnitPlatform()
}

// 3.  Qtype 생성 경로 추가
def querydslDir = "$buildDir/generated/querydsl"

// 4. 추가
querydsl { 
	jpa = true
	querydslSourcesDir = querydslDir
}

// 5. 추가
sourceSets { 
	main.java.srcDir querydslDir
}

// 6. 추가
configurations { 
	compileOnly {
		extendsFrom annotationProcessor
	}
	querydsl.extendsFrom compileClasspath
}

// 7. 추가
compileQuerydsl {
	options.annotationProcessorPath = configurations.querydsl
}
  1. Gradle reload 하기

(2) Q 클래스 생성

  1. Gradle 탭 > other > compileQuerydsl > run 하기

  2. Pakage에서 디렉터리 & Q클래스가 생긴 것을 볼 수 있다.

(3) EntityManager 주입받기

EntityManager 를 주입받음으로써, QueryDsl 이 query 를 생성할 수 있도록 한다.

  1. QueryDslConfig 생성

    /**
     * <h1>QuerydslConfig</h1>
     * <p>
     *     QueryDsl 설정
     * </p>
     * @author younghocha
     */
    @Configuration
    public class QuerydslConfig {
    
        @PersistenceContext
        private EntityManager entityManager;
    
        @Bean
        public JPAQueryFactory jpaQueryFactory(){
            return new JPAQueryFactory(entityManager);
        }
    }
  2. RoomRepository 생성

    public interface RoomRepository extends JpaRepository<Room, Long>, RoomRepositoryCustom {
    
    }
  3. RoomRepositoryCustom 생성

    public interface RoomRepositoryCustom {
        Page<RoomOfList> searchAll(FieldsOfRoomList fieldsOfRoomList, Pageable pageable);
    }
    • RoomRepository(JPARepository, RoomRepository)인 DAO 를 1개만 사용하기 위해서 생성
    • 쿼리를 수행할 메소드들을 정의한 인터페이스
      → 쿼리 수행에 필요한 파라메터, 반환값, 메소드명을 정의
  4. RoomRepositoryImpl

    /**
     * <h1>RoomRepositoryImpl</h1>
     * <p>
     * 방 필터링(동적쿼리)를 위한 클래스
     * </p>
     * @author younghoCha
     */
    
    @RequiredArgsConstructor
    public class RoomRepositoryImpl implements RoomRepositoryCustom{
    
        // 의존성 주입
        private final JPAQueryFactory queryFactory;
        private final ParsingEntityUtils parsingEntityUtils;
    
        @Override
        // searchAll : Room 필터링 과정 중에 필요한 동적 쿼리를 처리하기 위한 메소드를 오버라이드 한 것
        public Page<RoomOfList> searchAll(FieldsOfRoomList fieldsOfRoomList, Pageable pageable) {
    
          // QueryDSL 부분 : Query DSL을 사용하여 Join을 수행
          List<Room> result = queryFactory
                    .selectFrom(room)		// "room"이 저장되어 있는 테이블로 부터 조회한다
                    .leftJoin(room.tags, tag1).fetchJoin()	// leftJoin(room.tags, tag1) : room의 tag와 tag1을 leftJoin한다. fetchJoin() : 3번의 leftJoin을 패치 조인을 적용한다
                    // 주석 1. where() : 여러가지의 조건들을 입력한다.
                    .where(eqInterests(
                        fieldsOfRoomList.getExercise()),
                        eqArea(fieldsOfRoomList.getArea()),
                        participateCount(fieldsOfRoomList.getParticipantCount(), fieldsOfRoomList.isContainNoAdmittance()),
                        betweenTime(fieldsOfRoomList.getStartAppointmentDate(), fieldsOfRoomList.getEndAppointmentDate(), fieldsOfRoomList.isContainTimeClosing()),
                        eqTitle(fieldsOfRoomList.getRoomTitle()),
                        eqContent(fieldsOfRoomList.getRoomContent())
                    )
                    .offset(pageable.getOffset())		// 데이터를 가져올 레코드의 시작점을 결정해준다.
                    .limit(pageable.getPageSize())	// 가져올 레코드의 개수를 정한다.
                    .orderBy(sortRoomList(pageable))		// orderBy() : 파라미터로 "OrderSpecifier<?>" 타입을 받아서, 나온 값들을 정렬해준다. → 입력해준 값에 따라서 Query의 "Order By" 구문을 생성
                    .fetch();		// 주석 2. query를 생성하고, 결과를 리스트로 반환
    
            return afterTreatment(result);
    
        }
        //방 제목 검색
        private BooleanExpression eqTitle(String searchTitle){
            return searchTitle == null ? null : room.roomTitle.contains(searchTitle);
        }
    
        //지역 필터링 검색
        private BooleanBuilder eqArea(List<String> areas){
    
            if(areas == null){
                return null;
            }
    
            BooleanBuilder booleanBuilder = new BooleanBuilder();
    
            for (String area : areas){
                booleanBuilder.or(room.roomArea.contains(area));
            }
    
            return booleanBuilder;
        }
    
        //종목 필터링(복수) 검색
        private BooleanBuilder eqInterests(List<String> interests){
            if(interests == null){
                return null;
            }
    
            BooleanBuilder booleanBuilder = new BooleanBuilder();
    
            for (String interest : interests){
                booleanBuilder.or(room.exercise.eq(interest));
            }
    
            return booleanBuilder;
        }
    
        //방 설명 검색
        private BooleanExpression eqContent(String content){
            return content == null ? null : room.roomContent.contains(content);
        }
    
        // 시간 대 검색
        private BooleanExpression betweenTime(LocalDateTime start, LocalDateTime end, boolean conTainTimeClosing){
            /*시간 마감 보기 설정 */
            if(conTainTimeClosing){
                //시간 검색 X
                if(start == null){
                    return null;
                }
                //시간 검색 O
                return room.startAppointmentDate.between(start, end);
            }
    
            /*시간 마감 보기 설정 X*/
            //시간 검색 X
            if(start == null){
                return room.endAppointmentDate.gt(LocalDateTime.now());
            }
            //시간 검색 O
            return room.startAppointmentDate.between(start, end).and(room.endAppointmentDate.gt(LocalDateTime.now()));
        }
    
        /*입장 가능 인원 검색*/
        private BooleanExpression participateCount(Integer participantCount, boolean containNoAdmittance){
            //입장 마감 보기 시,
            if(containNoAdmittance){
                // 인원 검색을 하지 않았을 때
                if(participantCount == null){
                    return null;
                }
    
                // 인원 검색을 했을 때
                return room.limitPeopleCount.gt(room.participantCount.add(participantCount));
    
            }
    
            /*입장 마감 안보기 시,*/
            // 인원 검색을 하지 않았을 때
            if(participantCount == null){
                return room.limitPeopleCount.gt(room.participantCount);
            }
            // 인원 검색을 했을 때
            return room.limitPeopleCount.gt(room.participantCount.add(participantCount));
    
        }
    
        // 정렬
        private OrderSpecifier<?> sortRoomList(Pageable page){
            //서비스에서 보내준 Pageable 객체에 정렬조건 null 값 체크
            if (!page.getSort().isEmpty()) {
                //정렬값이 들어 있으면 for 사용하여 값을 가져온다
                for (Sort.Order order : page.getSort()) {
                    // 서비스에서 넣어준 DESC or ASC 를 가져온다.
                    Order direction = order.getDirection().isAscending() ? Order.ASC : Order.DESC;
                    // 서비스에서 넣어준 정렬 조건을 스위치 케이스 문을 활용하여 셋팅하여 준다.
                    switch (order.getProperty()){
                        case "start":
                            return new OrderSpecifier(direction, room.startAppointmentDate);
                        case "updatedTime":
                            return new OrderSpecifier(direction, room.updatedTime);
                        case "participant":
                            return new OrderSpecifier(direction, room.participantCount);
                    }
                }
            }
            return new OrderSpecifier(Order.DESC, room.updatedTime);
        }
    
        /**
         * List<Tag> -> List<String>으로 교체하기 위한 후처리 메소드
         * @param entities : 동적 쿼리로 조회된 Room Entity List
         * @return : 데이터를 조작한 Room DTO List
         * @author : younghoCha
         */
        private PageImpl afterTreatment(List<Room> entities){
    
            List<RoomOfList> rooms = new ArrayList<>();
            for (Room room : entities){
                List<String> tag = parsingEntityUtils.parsingTagEntityToString(room.getTags());
                rooms.add(RoomOfList.of(room, tag));
            }
    
            return new PageImpl<>(rooms);
        }
    }
    • QueryDsl에 대한 로직을 작성
    • DB를 상황에 맞게 Select하는 Query를 생성해준다.
    • 인터페이스를 구현한 클래스

    주석 1

    1. where()

       public Q where(Predicate... o) {
               return queryMixin.where(o);
           }

      1) where() 함수는 Predicate 타입의 객체를 받는다.
      2) Predicate로 받으면, query를 Predicate에 맞춰서 생성해준다.
      3) 생성하려는 조건 개수대로 Predicate를 생성해서 파라미터로 넘겨주면, 그에 맞는 Query가 생성된다.
      만약 null을 파라미터로 넘겨주면, 해당하는 파라미터를 무시하고 where 조건을 생성하지 않는다.

    2. where에 Predicate 객체를 넘기는 방법
      1) BooleanExpression 리턴 : 1번만 조회할 때 사용

         public abstract class BooleanExpression extends LiteralExpression<Boolean> implements Predicate {
         }

      2) BooleanBuilder 리턴 : 여러가지의 값을 같은 조건에 넣어야 할 때 사용

         public final class BooleanBuilder implements Predicate, Cloneable  {
         }

    주석 2
    fetch() 를 따라가면, 다음이 나온다.

    @Override
    @SuppressWarnings("unchecked")
    public List<T> fetch() {
        try {
            Query query = createQuery();
            return (List<T>) getResultList(query);
        } finally {
            reset();
        }
    }

참고: [JPA] Querydsl 기본
참고: [JPA] Spring Data JPA와 QueryDSL 이해, 실무 경험 공유
참고: Spring Boot에 QueryDSL을 사용해보자
참고: [Spring] QueryDSL 완벽 이해하기
참고: 주니어 개발자의 QueryDSL 찔러보기
참고: Spring Data JPA + Query DSL 사용기

profile
개발자로 거듭나기!

0개의 댓글