ABOUT ME

-

Today
-
Yesterday
-
Total
-
  • [Spring Boot] Spring Data JPA + QueryDSL 설정
    Spring Boot/기타 2023. 1. 15. 11:35
    반응형

    QueryDSL 이란?

    쿼리를 문자가 아닌 코드로 작성해도, 쉽고 간결하며 그 모양도 쿼리와 비슷하게 개발 할 수 있는 프로젝트가 바로 QueryDSL입니다. QueryDSL도 Criteria처럼 JPQL 빌더 역할을 하는데 JPA Criteria를 대체할 수 있습니다.

    QueryDSL은 오픈소스 프로젝트이며, 처음에는 HQL(Hibernate Query Language)을 코드로 작성할 수 있도록 해주는 프로젝트로 시작해서 지금  JPA, JDO, JDBC, Lucene, Hibernate Search, MongoDB, Collections 및 RDFBean을 지원합니다.

     

    참고 - http://querydsl.com/static/querydsl/4.4.0/reference/html_single/

     

    QueryDSL 설정

    1. gradle 설정

     

    build.gradle

    plugins {
        id 'java'
        id 'org.springframework.boot' version '2.7.7'
        id 'io.spring.dependency-management' version '1.0.15.RELEASE'
    }
    
    group = 'com.example'
    version = '0.0.1-SNAPSHOT'
    sourceCompatibility = '11'
    
    configurations {
        compileOnly {
            extendsFrom annotationProcessor
        }
    }
    
    repositories {
        mavenCentral()
    }
    
    dependencies {
    
        // Spring Boot
        implementation 'org.springframework.boot:spring-boot-starter-web'
        implementation 'org.springframework.boot:spring-boot-starter-data-jpa'
        testImplementation 'org.springframework.boot:spring-boot-starter-test'
    
        // Data
        implementation 'mysql:mysql-connector-java'
    
        // QueryDSL 라이브러리
        implementation 'com.querydsl:querydsl-core'
        // QueryDSL JPA 라이브러리
        implementation 'com.querydsl:querydsl-jpa'
        // QueryDSL 관련된 쿼리 타입(QClass)을 생성할 때 필요한 라이브러리로, annotationProcessor을 사용하여 추가
        annotationProcessor("com.querydsl:querydsl-apt:${dependencyManagement.importedProperties['querydsl.version']}:jpa")
        // java.lang.NoClassDefFoundError(javax.annotation.Entity) 발생 시 추가
        annotationProcessor 'jakarta.persistence:jakarta.persistence-api'
        // java.lang.NoClassDefFoundError(javax.annotation.Generated) 발생 시 추가
        annotationProcessor 'jakarta.annotation:jakarta.annotation-api'
    
        // Lombok
        compileOnly 'org.projectlombok:lombok'
        annotationProcessor 'org.projectlombok:lombok'
        testCompileOnly 'org.projectlombok:lombok'
        testAnnotationProcessor 'org.projectlombok:lombok'
    }
    
    tasks.named('test') {
        useJUnitPlatform()
    }
    
    // clean task를 실행 시 QClass를 삭제
    clean {
        // QClass가 생성되는 위치
        delete file('src/main/generated')
    }

    annotationProcessor는 자바 컴파일러 플러그인 일종으로, 컴파일 단계에서 프로젝트 내의 @Entity(javax.persistence.Entity) 애노테이션을 선언한 클래스를 탐색하여 com.querydsl.apt.jpa.JPAAnnotationProcessor를 통해 쿼리 타입(QClass)을 생성합니다. 생성된 쿼리 타입(QClass)은 com.querydsl.core.QueryFactory에 주입하여 사용합니다.

     

    참고 - http://honeymon.io/tech/2020/07/09/gradle-annotation-processor-with-querydsl.html

     

    2. Configuration 설정

    JPAQueryFactory를 주입받아 QueryDSL을 사용할 수 있도록 설정하세요.

     

    QueryDslConfig.java

    package com.example.querydsl.config;
    
    import com.querydsl.jpa.impl.JPAQueryFactory;
    import javax.persistence.EntityManager;
    import javax.persistence.PersistenceContext;
    import org.springframework.context.annotation.Bean;
    import org.springframework.context.annotation.Configuration;
    import org.springframework.data.jpa.repository.config.EnableJpaAuditing;
    
    @EnableJpaAuditing
    @Configuration
    public class QueryDslConfig {
    
      @PersistenceContext
      private EntityManager entityManager;
    
      public QueryDslConfig() {
      }
    
      @Bean
      public JPAQueryFactory jpaQueryFactory() {
        return new JPAQueryFactory(this.entityManager);
      }
    }

     

    3. Spring Data Jpa Custom Repository 적용

    Spring Data Jpa에서는 Custom Repository를 JpaRepository 상속 클래스에서 사용할 수 있도록 지원합니다.

    • TodoRepository는 JpaRepository 인터페이스를 상속받아서 기본적인 메서드를 사용할 수 있도록 정의한 인터페이스
    • TodoRepositoryCustom은 QueryDSL을 사용하기 위한 메소드를 정의한 인터페이스
    • TodoRepositoryImpl를 TodoRepositoryCustom 인터페이스에서 정의된 메소드를 구현하는 클래스

     

    참고 - https://docs.spring.io/spring-data/jpa/docs/3.0.x/reference/html/#repositories.custom-implementations

     

    Todo.java

    package com.example.querydsl.domain.entity;
    
    import javax.persistence.Column;
    import javax.persistence.Entity;
    import javax.persistence.GeneratedValue;
    import javax.persistence.GenerationType;
    import javax.persistence.Id;
    import javax.persistence.Table;
    import lombok.AllArgsConstructor;
    import lombok.Builder;
    import lombok.Getter;
    import lombok.NoArgsConstructor;
    import lombok.Setter;
    import lombok.ToString;
    
    @Getter
    @Setter
    @Builder
    @AllArgsConstructor
    @NoArgsConstructor
    @ToString
    @Entity
    @Table(name = "todo")
    public class Todo {
    
      @Id
      @GeneratedValue(strategy = GenerationType.IDENTITY)
      @Column(name="id", nullable = false)
      private Long id;
    
      @Column(name="title")
      private String title;
    
      @Column(name="description")
      private String description;
    
      @Column(name="completed")
      private Boolean completed;
    }


    TodoRepository.java

    package com.example.querydsl.domain.repository;
    
    import com.example.querydsl.domain.entity.Todo;
    import org.springframework.data.jpa.repository.JpaRepository;
    
    public interface TodoRepository extends JpaRepository<Todo, Long>, TodoRepositoryCustom {
    
    }


    TodoRepositoryCustom.java

    package com.example.querydsl.domain.repository;
    
    import com.example.querydsl.domain.entity.Todo;
    import java.util.List;
    
    public interface TodoRepositoryCustom {
    
      /**
       * QueryDSL을 사용하여 To-Do 목록 조회
       */
      List<Todo> getTodos(Todo todo);
    }


    TodoRepositoryImpl.java

    package com.example.querydsl.domain.repository;
    
    import com.example.querydsl.domain.entity.QTodo;
    import com.example.querydsl.domain.entity.Todo;
    import com.querydsl.jpa.impl.JPAQueryFactory;
    import java.util.List;
    import lombok.RequiredArgsConstructor;
    import org.springframework.stereotype.Repository;
    
    @RequiredArgsConstructor
    @Repository
    public class TodoRepositoryImpl implements TodoRepositoryCustom {
    
      private final JPAQueryFactory jpaQueryFactory;
    
      @Override
      public List<Todo> getTodos(Todo todo) {
        QTodo qtodo = QTodo.todo;
        return jpaQueryFactory
            .selectFrom(qtodo)
            .fetch();
      }
    }

     

    4. 단위 테스트

    TodoRepositoryTest.java

    package com.example.querydsl.domain.repository;
    
    import static org.junit.jupiter.api.Assertions.assertTrue;
    
    import com.example.querydsl.domain.entity.Todo;
    import java.util.List;
    import javax.persistence.EntityManager;
    import lombok.extern.slf4j.Slf4j;
    import org.junit.jupiter.api.BeforeEach;
    import org.junit.jupiter.api.Disabled;
    import org.junit.jupiter.api.DisplayName;
    import org.junit.jupiter.api.Test;
    import org.springframework.beans.factory.annotation.Autowired;
    import org.springframework.boot.test.context.SpringBootTest;
    import org.springframework.test.context.ActiveProfiles;
    import org.springframework.transaction.annotation.Transactional;
    
    @Slf4j
    @SpringBootTest
    @ActiveProfiles("local")
    @Disabled
    class TodoRepositoryTest {
    
      @Autowired
      private TodoRepository todoRepository;
    
      @Autowired
      EntityManager entityManager;
    
      @BeforeEach
      void init() {
        setUpTodos();
      }
    
      @Transactional
      @DisplayName("getTodos_QueryDSL을 사용하여 To-Do 목록 조회")
      @Test
      void testGetTodos() {
    
        // Given
        Todo todo = Todo.builder()
            .title("Title Test")
            .description("Description Test")
            .completed(true)
            .build();
    
        // When
        List<Todo> todos = todoRepository.getTodos(todo);
    
        // Then
        log.debug("todos:[{}]", todos);
        assertTrue(!todos.isEmpty());
      }
    
      /**
       * To-Do 목록을 설정
       */
      @Disabled
      void setUpTodos() {
    
        String title;
        String description;
        Boolean completed;
    
        for (int a = 1; a <= 100; a++) {
    
          title = "Title Test" + a;
          description = "Description Test" + a;
          if (a % 2 == 0) {
            completed = true;
          } else {
            completed = false;
          }
          insertTodo(title, description, completed);
        }
    
        entityManager.flush();
        entityManager.clear();
      }
    
      /**
       * To-Do 한 건을 저장
       */
      @Disabled
      void insertTodo(
          String title,
          String description,
          Boolean completed) {
        todoRepository.save(
            Todo.builder()
                .title(title)
                .description(description)
                .completed(completed)
                .build());
      }
    }

     

    5. 단위 테스트 확인

    testGetTodos() 실행 시 To-Do 목록이 조회되는지 확인하세요.

     

    소스 코드는 Github Repository - https://github.com/tychejin1218/blog/tree/main/query-dsl 를 참조하세요.

    반응형

    댓글

Designed by Tistory.