ABOUT ME

-

Today
-
Yesterday
-
Total
-
  • QueryDSL
    🌱 spring 2022. 10. 5. 18:57

    ❓QueryDSL


    💡 QueryDSL은 정적 타입을 이용해 SQL과 같은 쿼리를 생성할 수 있도록 해주는 프레임워크

     

    사용하는 이유?

    • 실제로 Query를 작성하다보면 수 많은 쿼리를 수작업으로 생성해야 한다.
    • 또한, 사람이 작성하면 Query는 컴파일 단계에서 오류가 있는지 없는지 알 수 없다.

    JPQL

    JPQL ( Java Persistence Query Language ) 는 엔티티 객체를 조회하는 객체지향 쿼리이다.

    • 테이블이 아닌 객체를 검색하는 객체지향 쿼리
    • SQL을 추상화 하여 특정 데이터베이스에 의존하지 않는다.
      • 데이터베이스의 방언이 바뀌어도 수정하지 않아도 된다.
    • JPA는 JPQL을 분석하여 SQL을 생성한 후 DB에서 조회한다.

    특징

    1. SQL을 추상화한 JPA의 객체지향 쿼리
    2. Table이 아닌 Entity 객체를 대상으로 개발
    3. Entity와 속성은 대소문자 구분
    4. 별칭(alias) 사용이 필수

    JPQL의 한계

    • JPQL에서 복잡한 쿼리를 작성하다보면 오타가 발생할 수 있고, 가독성이 떨어질 수 있다.
      • 문자열(String) 형태이기 때문에 개발자 의존적
    • Compile 단계에서 Type-Check 가 불가능하다.
    • Runtime 단계에서 오류를 발견할 수 있다.

    Criteria 쿼리

    Criteria는 JPQL을 자바 코드로 작성하도록 도와주는 빌더 클래스 API이다.

    ⇒ 문자가 아닌 프로그래밍 코드로 JPQL을 작성할 수 있다

     

    장점

    • 문법 오류를 컴파일 단계에서 확인이 가능하다.
    • 문자 기반의 JPQL보다 동적 쿼리를 안전하게 생성할 수 있다.

    단점

    • 코드가 복잡해 직관적이지 못해 이해하기 어렵다.
    // JPQL : select m from Member m
    
    // Criteria 쿼리 빌더 
    CriteriaBuilder cb = em.getCriteriaBuilder();  
    
    // Criteria 생성 + 반환 타입 지정
    CriteriaQuery<Member> cq = cb.createQuery(Member.class);
    
    // FROM 절
    Root<Member> m = cq.from(Member.class); 
    
    // SELECT 절
    cq.select(m);
    
    TypedQuery<Member> query = em.createQuery(cq);
    List<Member> members = query.getResultList();
    
    • Criteria 쿼리는 빌더(CriteriaBuilder)가 필요하다.
      • EntityManager 또는 EntityManagerFactory로 생성한다.
    • Criteria 쿼리 빌더에서 Criteria 쿼리 (CriteriaQuery)를 생성한다.
    • FROM 절을 생성하고,
    • 반환된 값은 Criteria에서 사용하는 별칭인, 조회의 시작점이라는 의미로 쿼리 루트(Root)로 사용한다.
    • SELECT 절 생성
    // 검색 조건 추가 시
    //JPQL
    /*
    		select m from Member m
    		where m.username = '회원1'
    		order by m.age desc
    */
    
    CriteriaBuilder cb = em.getCriteriaBuilder();
    
    CriteriaQuery<Member> cq = cb.createQuery(Member.class);
    Root<Member> m = cq.from(Member.class);
    
    // 검색 조건 정의
    Predicate usernameEqual = cb.equal(m.get("username"), "회원1");
    
    // 정렬 조건 정의
    javax.persistence.criteria.Order ageDesc = cb.desc(m.get("age"));
    
    // 쿼리 생성
    cq.select(m)
    	.where(usernameEqual) //WHERE 절 생성
        .orderby(ageDesc); //ORDER BY 절 생성
    
    List<Member> resultList = em.createQuery(cq).getResultList();

    ⇒ 굉장히 복잡하다.

    최근에는 Criteria를 거의 사용하지 않는다.

    ❗ QueryDSL


    Criteria의 복잡하고 어렵다는 단점을 개선하기 위해 나온 프레임워크

    QueryDSL은 JPQL의 빌더 역할 을 하고, JPA Criteria를 대체 할 수 있다.

    특징

    1. 문자가 아닌 코드로 쿼리를 작성 ⇒ 컴파일 시점에 문법 오류를 쉽게 발견할 수 있다.
    2. 자동완성, IDE 의 도움을 받을 수 있다.
    3. 동적인 쿼리 작성이 편리
    4. 쿼리 작성 시 제약 조건 등을 메서드화하여 재사용할 수 있다.
      • BooleanBuilder
    5. 도메인 타입과 프로퍼티를 안전하게 참조할 수 있다.
      • 도메인 타입의 리팩터링을 더 잘 할 수 있다.

    Gradle 설정

    Gradle을 사용한 프로젝트의 QueryDSL 환경 설정에서 중요한 것

    1. Spring boot 버전
    2. Gradle 버전
    3. QueryDSL 버전
    • 기존에 작업중인 Spring boot 프로젝트의 버전을 확인하여 버전에 맞는 설정을 하는 것이 중요하다.

    springframework.boot - 2.7.2
    gradle - 7.5
    java - 11

    build.gradle

    // 1. queryDsl version 정보 추가
    buildscript {
        ext {
            queryDslVersion = "5.0.0"
        }
    }
    plugins {
        id 'org.springframework.boot' version '2.7.2'
        id 'io.spring.dependency-management' version '1.0.12.RELEASE'
    		// 2. querydsl plugins 추가
        id "com.ewerk.gradle.plugins.querydsl" version "1.0.10"
        id 'java'
    }
    
    group = 'com.prgrms'
    version = '0.0.1-SNAPSHOT'
    sourceCompatibility = '11'
    
    configurations {
        compileOnly {
            extendsFrom annotationProcessor
        }
    }
    
    repositories {
        mavenCentral()
    }
    
    dependencies {
        implementation 'org.springframework.boot:spring-boot-starter-data-jpa'
        compileOnly 'org.projectlombok:lombok'
        runtimeOnly 'com.h2database:h2'
        runtimeOnly 'mysql:mysql-connector-java'
        annotationProcessor 'org.projectlombok:lombok'
        testImplementation 'org.springframework.boot:spring-boot-starter-test'
    
    		// 3. querydsl dependencies 추가
        // 3-1. querydsl 추가 - QuerydslRepository, QueryDslPredicateExecutor 사용 가능
        implementation "com.querydsl:querydsl-jpa:${queryDslVersion}"
    		// 3-2. querydsl-apt : 쿼리 타입(Q)를 생성할 때 필요한 라이브러리
        annotationProcessor "com.querydsl:querydsl-apt:${queryDslVersion}"
    }
    
    tasks.named('test') {
        useJUnitPlatform()
    }
    
    // 4. queryDSL 설정 추가
    
    // 4-1. querydsl에서 사용할 경로 설정
    def querydslDir = "$buildDir/generated/querydsl"
    // 4-2. JPA 사용여부와 사용할 경로 설정
    querydsl {
        jpa = true
        querydslSourcesDir = querydslDir
    }
    // 4-3. build시 사용할 sourceSet 추가
    sourceSets {
        main.java.srcDir querydslDir
    }
    // 4-4. querydsl 컴파일시 사용할 옵션 설정
    compileQuerydsl {
        options.annotationProcessorPath = configurations.querydsl
    }
    // 4-5. querydsl이 compileClassPath를 상속하도록 설정
    configurations {
        querydsl.extendsFrom compileClasspath
    }
    
    • annotationProcessorJava 컴파일러 플러그인으로서, 컴파일 단계에서 프로젝트 내의 @Entity 어노테이션을 선언한 클래스를 탐색하고 JPAAnnotationProcessor를 통해 쿼리 타입(QClass)를 생성한다.

    쿼리 타입(QClass)들은 QueryDSL을 사용하여 메서드 기반으로 쿼리를 작성할 때 프로젝트에서 만든 도메인 클래스(Entity)의 구조를 설명해주는 메타데이터 역할을 하며, 쿼리의 조건을 설정할 때 사용

    QClass 생성

    Gradle → other > compileQuerydsl → run

    ⇒ package에서 디렉터리가 생기고 QClass가 생긴다.

    QuerydslConfig.java

    해당 클래스를 통해 JpaQueryFactory를 bean으로 등록하여 repository에서 필요할 때마다 생성해서 쓰는 것이 아니라 바로 가져와서 사용할 수 있도록 한다.

    • Querydsl을 사용하여 쿼리를 build하기 위해서는 JpaQueryFactory가 필요하다
    • JpaQueryFactory를 사용하면 EntityManager를 통해 질의가 처리 된다.
    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;
    
    @Configuration
    @EnableJpaAuditing
    public class QuerydslConfig {
    
        @PersistenceContext
        private EntityManager entityManager;
    
        public querydslConfig() {
        }
    
        @Bean
        public JPAQueryFactory jpaQueryFactory() {
            return new JPAQueryFactory(this.entityManager);
        }
    }
    
    • JPAQueryFactory
      • 쿼리 및 DML 절 생성을 위한 팩토리 클래스
      • EntityManager를 파라미터로 넘겨줘야 한다.

    User Entity / Repository

    User.class

    @Table(name = "user")
    @Entity
    @Getter
    @Builder
    @AllArgsConstructor
    @NoArgsConstructor
    public class User {
    
      @Id
      @GeneratedValue(strategy = GenerationType.IDENTITY)
      @Column(name = "idx", nullable = false)
      private Long id;
    
      @Column(name = "name")
      private String name;
    
      @Column(name = "age")
      private int age;
    
      @Column(name = "email")
      private String email;
    }

    UserRepository Interface

    public interface UserRepository extends JpaRepository<User, Long>, 
    																				UserRepositoryCustom {}
    

    UserRepositoryCustom Interface

    public interface UserRepositoryCustom {
      List<User> getUserList();
    }
    

    UserRepositoryImpl.class

    @Repository
    public class UserRepositoryImpl implements UserRepositoryCustom {
    
      private final JPAQueryFactory queryFactory;
    
      public UserRepositoryImpl(JPAQueryFactory queryFactory) {
        this.queryFactory = queryFactory;
      }
    
      QUser user = QUser.user;
    	// QUser u = new Quser("u");  - (1)
    
      @Override
      public List<User> getUserList() {
        return queryFactory
            .selectFrom(user)
            .fetch();
      }
    }
    

    (1) - querydsl 에서 사용하기 위한 Q-Type 생성, 파라미터로 별칭(alias)을 넘겨줄 수 있다.

    UserRepository : JpaRepository를 사용하여 메서드의 이름으로 간단한 메서드들을 구현하는 용도

    UserRepositoryCustom : querydsl을 통해 사용하기 위한 쿼리를 정의하는 interface

    UserRepositoryImpl : Custom interface에서 정의된 메서드를 오버라이드하여 실제로 구현하는 클래스

    ⇒ 실제로 사용하는 곳에서는 Repository 하나만을 가지고 모든 메서드를 사용할 수 있다.

    Querydsl 사용방법

    Q-Type

    Q-Type 인스턴스를 사용하는 2가지 방법

    QUser qUser = new Quser("u");  // 별칭 직접 지정
    QUser qUser = QUser.user;      // 기본 인스턴스 사용
    
    • 직접 별칭을 넣어서 사용해도 되지만 보통 기본 인스턴스에서 생성된 별칭을 사용하는 것을 권장
    • 별칭 직접 사용 ⇒ 같은 테이블을 조인하는 경우 사용

    검색 조건 쿼리

    JPQL이 제공하는 검색 조건을 거의 대부분 제공한다.

    SimpleExpression에서 제공하는 검색 조건 메서드 확인 가능하다.

    user.name.eq("beomsic")          // name = "beomsic"
    user.name.ne("beomsic")          // name != "beomsic"
    user.name.eq("beomsic").not()    // name != "beomsic"
    user.name.isNotNull()            // name is not null
    
    user.age.in(10, 20)              // age in (10, 20)
    user.age.notIn(10, 20)           // age not in (10, 20)
    user.age.between(10, 30)         // age between (10, 30)
    
    user.age.goe(30)                 // age >= 30
    user.age.gt(30)                  // age > 30
    user.age.loe(30)                 // age <= 30
    user.age.lt(30)                  // age < 30
    
    user.name.like("beomsic%")       // like 검색
    user.name.contains("beoms")      // like '%beoms%' 검색
    user.name.startsWith("beoms")    // like 'beoms%' 검색
    

    Where 문에서 AND 조건을 파라미터로 처리가 가능하다.

    • where() 에 파라미터로 검색조건을 추가하면 AND 조건으로 추가 된다.

    결과 조회

    • fetch()
      • 리스트 조회
      • 데이터가 없으면 빈 리스트 반환 ( NULL ❌ )
    • fetchOne()
      • 단 건 조회
      • 결과가 없다면 null
      • 결과가 둘 이상이면 NonUniqueResultException 발생
    • fetchFirst()
      • limit(1).fetchOne() 과 같다.
    • fetchResults()
      • 페이징 정보 포함
      • total count 쿼리 추가 실행
    • fetchCount()
      • count 쿼리로 변경하여 count 수 조회

    정렬

    • desc()
      • 내림차순
    • asc()
      • 올림차순
    • nullsLast()
      • null 데이터 순서 부여(마지막)
    • nullsFirst()
      • null 데이터 순서 부여(처음)

    페이징

    • offset(long offset)
      • 쿼리 결과에 대한 오프셋을 정의
      • 0부터 시작
    • limit(long limit)
      • 쿼리 결과에 대한 제한/최대 결과를 정의

    집합

    집합 함수

    • count()
    • sum()
    • avg()
    • max()
    • min()

    그룹화 / 집계

    • groupby()
    • having()

    조인

    • join(), innerJoin()
    • leftJoin()
    • rightJoin()
    • fullJoin()

    JPQL의 on과 성능 최적화를 위한 fetch 조인 제공

    기본 문법 : join(조인 대상, 별칭으로 사용할 Q타입)

    서브 쿼리

    • JPASubQuery를 생성해서 사용
    • 결과가 하나면 unique(), 여러건이면 list() 사용

    동적 쿼리


    BooleanBuilder를 사용하면 동적 쿼리를 사용할 수 있다.

    where 절에서 사용하는 데이터 값이 null인 경우 조건을 제외시킬 수 있어 동적쿼리를 만들기 편리하다.

    User testUser = new  User();
    testUser.setName("beomsic");
    testUser.setAge(26);
    
    QUser user = QUser.user;
    
    BooleanBuilder builder = new BooleanBuilder();
    if (StringUtils.hasText(testUser.getName())) {
    	builder.and(user.name.contains(testUser.getName()));
    }
    if (testUser.getAge() != null) {
    	builder.and(user.age.gt(testUser.getAge()));
    }
    
    List<User> result = query.from(user)
    		.where(builder)
    		.list(user);

    메소드 위임


    메소드 위임 기능을 사용하면 쿼리 타입에 검색 조건을 직접 정의가 가능

    public class UserExpression {
      @QueryDelegate
      public static BooleanExpression isExpensive(QUser user, Integer age) {
    	return user.age.gt(age);
      }
    }

    쿼리 타입에 생성된 결과

    ...
    public class QUser extends EntityPathBase<User> {
    		...
      public BooleanExpression isExpensive(Integer age) {
         return UserExpression.isExpensive(this, age);
      }
    }

    빈 생성


    쿼리 결과를 엔티티가 아닌 특정 객체(ex : dto)로 받고 싶은 경우 빈 생성 기능을 사용한다.

    1. 프로퍼티 접근 (setter)
    2. 필드 직접 접근
    3. 생성자 사용

    UserDto.class

    // DTO 생성
    public class UserDTO {
      private String name;
      private int age;
      
      public UserDTO() {}
      
      public UserDTO(String name, int age) {
      	this.name = name;
    	this.age = age;
      }
      //Getter, Setter
      ...
    }

    프로퍼티 접근 (setter)

    Projections.bean

    UserDTO 에 name을 가지고 있기 때문에 as를 사용해 별칭을 지정

    QUser user = QUser.user;
    List<UserDTO> result = query.from(user).list(
    		Projections.bean(UserDTO.class, user.name.as("name"), user.age));
    

    필드 직접 접근

    Projections.fields

    필드에 직접 접근해서 값을 설정, 필드를 private로 설정해도 동작한다.

    QUser user = QUser.user;
    List<UserDTO> result = query.from(user).list(
    		Projections.fields(UserDTO.class, user.name.as("name"), user.age));
    

    생성자 사용

    Projections.constructor

    지정한 프로젝션과 파라미터 순서가 같은 생성자가 필요하다.

    QUser user = QUser.user;
    List<UserDTO> result = query.from(user).list(
    		Projections.constructor(UserDTO.class, user.name.as("name"), user.age));
    

    참고 자료

    https://steady-coding.tistory.com/582

    https://milenote.tistory.com/137

    https://wildeveloperetrain.tistory.com/92

    https://www.baeldung.com/rest-api-search-language-spring-data-querydsl

    '🌱 spring' 카테고리의 다른 글

    JPA - N + 1 문제  (0) 2022.10.11
    영속성 컨텍스트  (0) 2022.10.11
    JPA와 Hibernate, Spring data JPA  (0) 2022.10.10
    Spock 로 테스트 해보기  (0) 2022.10.06
    MVCC  (1) 2022.10.05

    댓글

Designed by Tistory.