ABOUT ME

-

Today
-
Yesterday
-
Total
-
  • QueryDSL + Projection
    BE/스프링 2024. 11. 13. 22:43

    QueryDSL이란?

    데이터베이스와 애플리케이션 간의 데이터를 안전하고 편리하게 주고받기 위한 Java 라이브러리
    데이터베이스 쿼리를 문자열 대신 코드로 작성할 수 있어 타입 안전성을 보장하고, 
    쿼리 작성 시 문법 오류를 컴파일 시점에 미리 발견할 수 있다는 것이 장점

     

     


     

    QueryDSL의 필요성

     

    기존의 Java ORM(객체-관계 매핑) 프레임워크인 JPA와 Hibernate는 SQL과 유사한 HQL(JPQL)을 사용해 쿼리를 작성하는데

    JPQL은 문자열로 쿼리를 작성하다 보니 오타를 쉽게 놓칠 수 있고, 쿼리 오류는 애플리케이션을 실행할 때 비로소 발견될 수 있어 위험할 수 있음.

     

    -> QueryDSL은 엔티티에 기반한 Q 클래스를 자동 생성하여 쿼리를 코드로 작성하게 해주어 오타나 잘못된 타입을 사용하면 컴파일 시점에 바로 오류를 확인 가능

     

     


     

    QueryDSL 설치 및 설정

    Maven

    pom.xml 파일에 다음 내용 추가

    <properties>
        <querydsl.version>5.0.0</querydsl.version> <!-- 사용하려는 QueryDSL 버전 -->
    </properties>
    
    <dependencies>
        <!-- QueryDSL APT (Annotation Processing Tool) -->
        <dependency>
            <groupId>com.querydsl</groupId>
            <artifactId>querydsl-apt</artifactId>
            <version>${querydsl.version}</version>
            <classifier>jakarta</classifier> <!-- JPA를 사용할 경우 jakarta로 설정 -->
            <scope>provided</scope> <!-- 컴파일 시에만 필요하므로 provided로 설정 -->
        </dependency>
    
        <!-- QueryDSL JPA -->
        <dependency>
            <groupId>com.querydsl</groupId>
            <artifactId>querydsl-jpa</artifactId>
            <version>${querydsl.version}</version>
            <classifier>jakarta</classifier>
        </dependency>
    </dependencies>

     

     

    • querydsl-apt: 컴파일 시점에 @Entity와 같은 어노테이션을 분석하여 쿼리 빌드를 위한 Q 클래스(메타데이터 클래스)를 생성
    • querydsl-jpa: QueryDSL의 JPA 모듈로, JPA 엔티티와 함께 QueryDSL을 사용 가능하게 함

     

    Q 클래스 생성 플러그인 추가

    apt-maven-plugin 플러그인을 설정하여 QueryDSL이 Q 클래스를 자동으로 생성

    <plugin>
        <groupId>com.mysema.maven</groupId>
        <artifactId>apt-maven-plugin</artifactId>
        <version>1.1.3</version>
        <executions>
            <execution>
                <goals>
                    <goal>process</goal>
                </goals>
                <configuration>
                    <outputDirectory>target/generated-sources/java</outputDirectory>
                    <processor>com.querydsl.apt.jpa.JPAAnnotationProcessor</processor>
                </configuration>
            </execution>
        </executions>
    </plugin>

     

    • outputDirectory : 생성된 Q 클래스가 저장될 위치 설정
    • processor : JPAAnnotationProcessor를 지정해 JPA 엔티티로부터 Q 클래스를 생성하도록 함

     

     

    Gradle

    plugins {
        id 'org.springframework.boot' version '2.5.3' // 사용 중인 버전에 맞게 수정하세요
        id 'io.spring.dependency-management' version '1.0.11.RELEASE'
        id 'java'
    }
    
    group = 'com.example'
    version = '0.0.1-SNAPSHOT'
    sourceCompatibility = '1.8'
    
    repositories {
        mavenCentral()
    }
    
    ext {
        queryDslVersion = "5.0.0" // 사용 중인 QueryDSL 최신 버전 확인
    }
    
    dependencies {
        implementation 'org.springframework.boot:spring-boot-starter-data-jpa'
        implementation "com.querydsl:querydsl-jpa:${queryDslVersion}"
        annotationProcessor "com.querydsl:querydsl-apt:${queryDslVersion}:jpa"
    }
    
    sourceSets {
        main {
            java {
                srcDirs = ["$projectDir/src/main/java", "$projectDir/build/generated"]
            }
        }
    }
    
    tasks.withType(JavaCompile) {
        options.annotationProcessorPath = configurations.annotationProcessor
    }

     

     

     

    • queryDslVersion: QueryDSL의 최신 버전을 지정
    • annotationProcessor: querydsl-apt는 컴파일 시점에 어노테이션을 처리하여 Q 클래스를 생성
    • sourceSets: build/generated 디렉토리에 생성된 Q 클래스를 사용하기 위해 소스 경로에 포함
    • tasks.withType(JavaCompile): annotationProcessorPath 설정을 통해 컴파일 시 annotationProcessor 경로 설정

     

     


     

     

    Q 클래스

    Q 클래스는 Entity 클래스와 1:1로 매칭되는 QueryDSL 전용 메타데이터 클래스
    Entity의 필드를 표현하는 *Path 타입의 필드를 포함하여 QueryDSL에서 사용할 수 있게 해줌.

     

     

     

    User 엔티티 -> QUser 클래스

     
    public static final QUser user = new QUser("user");
    public final NumberPath<Long> id = createNumber("id", Long.class);
    public final StringPath name = createString("name");
    public final BooleanPath disabled = createBoolean("disabled");

     

    • QUser.user: 쿼리에서 QUser의 기본 인스턴스로 사용
    • NumberPath, StringPath 등: 각 필드의 타입에 따라 자동 생성된 필드 경로(Path)

     


     

    QueryDSL 쿼리 예제

     

    1. 이름이 "Sonoopy"인 사용자 조회

    EntityManager em = entityManagerFactory.createEntityManager();
    JPAQueryFactory queryFactory = new JPAQueryFactory(em);
    
    QUser user = QUser.user;
    User result = queryFactory.selectFrom(user)
        .where(user.name.eq("Sonoopy"))
        .fetchOne();
     

     

    2. 여러 조건을 사용한 정렬 및 그룹화

     

    사용자 이름 기준으로 오름차순 정렬 조회

    List<User> users = queryFactory.selectFrom(user)
        .orderBy(user.name.asc())
        .fetch();

     

    그룹화를 통해 특정 컬럼으로 중복된 값의 개수를 조회

    NumberPath<Long> count = Expressions.numberPath(Long.class, "count");
    
    List<Tuple> titleCounts = queryFactory.select(blogPost.title, blogPost.id.count().as(count))
        .from(blogPost)
        .groupBy(blogPost.title)
        .orderBy(count.desc())
        .fetch();

     

     

    3. 조인과 서브쿼리

     

    조인: "Hello World!"라는 제목의 게시글을 작성한 유저를 조회

    List<User> users = queryFactory.selectFrom(user)
        .innerJoin(user.blogPosts, blogPost)
        .on(blogPost.title.eq("Hello World!"))
        .fetch();

     

     

    서브쿼리: 같은 조건을 서브쿼리로 작성

    List<User> users = queryFactory.selectFrom(user)
        .where(user.id.in(
            JPAExpressions.select(blogPost.user.id)
                .from(blogPost)
                .where(blogPost.title.eq("Hello World!"))))
        .fetch();

     

     

     

    4. 데이터 수정 및 삭제

     

    업데이트

    queryFactory.update(user)
        .where(user.name.eq("Ash"))
        .set(user.name, "Ash2")
        .set(user.disabled, true)
        .execute();
     

     

    삭제

    queryFactory.update(user)
        .where(user.name.eq("Ash"))
        .set(user.name, "Ash2")
        .set(user.disabled, true)
        .execute();

     

     

    ** 데이터 삽입은 EntityManager persist 메서드를 사용

     

     

     


     

    Projections

    QueryDSL에서 엔티티 간의 연관관계가 없는 경우, 특정 컬럼들만 조인하여 조회할 때 DTO로 데이터를 매핑하여 반환할 수 있는데,
    Projections를 사용하면 다양한 방법으로 DTO에 값을 매핑 가능

     

     

     

    Projections를 활용한 DTO 매핑 방법

     

    Projections.bean

    Projections.fields

    Projections.constructor

    @QueryProjection

     

     

    엔티티 및 DTO 예제

    연관관계가 없는 AEntity와 BEntity -> 매핑 -> ABDTO 반환

    @Entity
    @Table(name = "Atable")
    @Getter
    @NoArgsConstructor
    public class AEntity {
        @Id
        private int id;
        private String tableName;
        private String regDate;
        private String memberId;
    }
    
    @Entity
    @Table(name = "Btable")
    @Getter
    @NoArgsConstructor
    public class BEntity {
        @Id
        private String memberId;
        private String branchNm;
        private String orgNm;
        private String name;
    }
    
    @Data
    @NoArgsConstructor @AllArgsConstructor
    public class ABDTO {
        private int id;
        private String tableName;
        private String regDate;
        private String memberId;
        private String branchName;
        private String name;
        private String departName;
    }

     

     


     

    1. Projections.bean

     

    Projections.bean은 DTO의 setter 메서드를 사용하여 값을 삽입하는 방식

    public List<ABDTO> findAll_bean() {
        return queryFactory
                .select(Projections.bean(ABDTO.class,
                        aEntity.id,
                        aEntity.tableName,
                        aEntity.memberId,
                        bEntity.branchNm.as("branchName"),
                        bEntity.name,
                        bEntity.orgNm.as("departName")
                ))
                .from(aEntity)
                .join(bEntity).on(aEntity.memberId.eq(bEntity.memberId))
                .fetch();
    }

     

    • 장점: 코드 가독성이 좋고, 필드 이름이 동일할 경우 간편하게 사용 가능.
    • 주의점: 필드 이름이 다를 경우 as로 매칭을 해줘야 하며, DTO에 setter 메서드 필요

     

     

    2. Projections.fields

    Projections.fields는 필드에 직접 데이터를 할당하는 방식

    public List<ABDTO> findAll_fields() {
        return queryFactory
                .select(Projections.fields(ABDTO.class,
                        aEntity.id,
                        aEntity.tableName,
                        aEntity.memberId,
                        bEntity.branchNm.as("branchName"),
                        bEntity.name,
                        bEntity.orgNm.as("departName")
                ))
                .from(aEntity)
                .join(bEntity).on(aEntity.memberId.eq(bEntity.memberId))
                .fetch();
    }
     
    • 장점: 필드에 직접 할당하므로 성능상 유리
    • 주의점: DTO의 필드와 네이밍이 다르면 as로 맞춰야 하며, 직관성이 떨어질 수 있음.

     


     

    3. Projections.constructor

     

    Projections.constructor는 DTO의 생성자를 통해 필드 값을 삽입

    public List<ABDTO> findAll_constructor() {
        return queryFactory
                .select(Projections.constructor(ABDTO.class,
                        aEntity.id,
                        aEntity.tableName,
                        aEntity.regDate,
                        aEntity.memberId,
                        bEntity.branchNm,
                        bEntity.name,
                        bEntity.orgNm
                ))
                .from(aEntity)
                .join(bEntity).on(aEntity.memberId.eq(bEntity.memberId))
                .fetch();
    }
     
    • 장점: 필드 순서만 맞다면 네이밍과 상관없이 매핑 가능.
    • 주의점: 필드가 많을 경우 순서를 맞추기 어려워 실수할 수 있습니다.

     


     

    4. @QueryProjection

    @QueryProjection 어노테이션을 DTO의 생성자에 추가하여 QueryDSL에서 자동으로 사용할 수 있도록 설정하는 방법

    @Data
    @NoArgsConstructor @AllArgsConstructor
    public class ABDTO {
        private int id;
        private String tableName;
        private String regDate;
        private String memberId;
        private String branchName;
        private String name;
        private String departName;
    
        @QueryProjection
        public ABDTO(String memberId, String name) {
            this.memberId = memberId;
            this.name = name;
        }
    }
    
    public List<ABDTO> findAll_Qdto() {
        return queryFactory
                .select(new QABDTO(aEntity.memberId, bEntity.name))
                .from(aEntity)
                .join(bEntity).on(aEntity.memberId.eq(bEntity.memberId))
                .fetch();
    }
     
    • 장점: QDTO를 통해 간편하게 사용 가능하며, 컴파일 시점에 오류를 검출할 수 있습니다.
    • 주의점: DTO가 QueryDSL에 의존하게 되며, 전역에서 사용하는 DTO에는 적합하지 않을 수 있습니다.

     

    'BE > 스프링' 카테고리의 다른 글

    JPA Auditing  (0) 2024.11.14
    JPA : Entity 연관 관계 / @JoinColumn  (1) 2024.11.04
Designed by Tistory.