framework/spring

7. 데이터 접근 기술 - Querydsl

wooweee 2023. 9. 30. 15:14
728x90

1. Querydsl 설정

1.1. build.gradle

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

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

// hibernate.version 버그로 인한 version downgrade 방법
ext["hibernate.version"] = "5.6.5.Final"

configurations {
    compileOnly {
        extendsFrom annotationProcessor
    }
}

repositories {
    mavenCentral()
}

dependencies {
    implementation 'org.springframework.boot:spring-boot-starter-thymeleaf'
    implementation 'org.springframework.boot:spring-boot-starter-web'
    compileOnly 'org.projectlombok:lombok'
    annotationProcessor 'org.projectlombok:lombok'
    testImplementation 'org.springframework.boot:spring-boot-starter-test'
    //테스트에서 lombok 사용
    testCompileOnly 'org.projectlombok:lombok'
    testAnnotationProcessor 'org.projectlombok:lombok'

    // JdbcTemplate 추가
    // mybatis나 jpa 사용시, 해당 패키지 내부에 jdbcTemplate이 들어가있어서 주석처리
    // implementation 'org.springframework.boot:spring-boot-starter-jdbc'

    // MyBatis 추가 - 김영한 강사님 file을 그대로 사용한 것이여서 2.2.0으로 작성해야한다.
    // boot 3.0 이상 사용시 version을 3.0.1로 변경 해야한다.
   implementation 'org.mybatis.spring.boot:mybatis-spring-boot-starter:2.2.0'

    // JPA 추가
   implementation 'org.springframework.boot:spring-boot-starter-data-jpa'

    // H2 데이터베이스 추가
    runtimeOnly 'com.h2database:h2'

    //Querydsl 추가
    implementation 'com.querydsl:querydsl-jpa'
    annotationProcessor "com.querydsl:querydsl-apt:${dependencyManagement.importedProperties['querydsl.version']}:jpa"
    annotationProcessor "jakarta.annotation:jakarta.annotation-api"
    annotationProcessor "jakarta.persistence:jakarta.persistence-api"
}

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

// Querydsl 추가, 자동 생성된 Q클래스를 gradle clean으로 제거 
clean {
    delete file('src/main/generated')
}

 

dependencies {
    //Querydsl 추가
    implementation 'com.querydsl:querydsl-jpa'
    annotationProcessor "com.querydsl:querydsl-apt:${dependencyManagement.importedProperties['querydsl.version']}:jpa"
    annotationProcessor "jakarta.annotation:jakarta.annotation-api"
    annotationProcessor "jakarta.persistence:jakarta.persistence-api"
}

// Querydsl 추가, 자동 생성된 Q클래스를 gradle clean으로 제거 
clean {
    delete file('src/main/generated')
}

 

1.2. Q 타입 생성 확인 방법

 

  • Q type은 compile 시점에 자동 생성되는 파일이기 때문에 Git에 포함하지 않는 것을 권장한다.
  • gradle build 하위에 생성되기 때문에 해당 패키지 자체를 git에 올라가지 않게 막으면 된다.

  • 참고 - Querydsl 설정 부분은 공식메뉴얼에 소개 되어 있는 부분이 아니기 때문에 IntelliJ version이 변경되거나 Querydsl의 Gradle 설정 version이 변경이 될 경우 적용 방법이 조금씩 달라진다.
    따라서 querydsl gradle로 검색하면 본인 환경에 맞는 대안을 찾아서 설정을 하길 권장한다.

 

1.2.1. gradle 이용 방법

  1. gradle 설정으로 gradle 선택

  2. 우측 배너에 아래 clean compile 2가지 단계 수행
    1. Gradle -> Task -> build -> clean
    2. Gradle -> Tasks -> other -> compileJava
  3. build -> generated -> sources -> annotationProcessor -> java/main 하위에 hello.itemservice.domain.QItem.class 가 생성되면 Q 타입이 생성이 된 것이다.

 

gradle 방식 QItem

 

gradle로 clean compile

 

 

1.2.2. IntelliJ 방식

  1. intelliJ로 변경
  2. ItemServiceApplication 실행  * db 실행 중 
  3. 동일 위치에 QItem 생성 확인 가능
  4. app 실행 대신 build -> Build Project로도 수행 가능 혹은 캐시 적용으로 generated package 생성 안될 경우 rebuild 하면 QItem 생성된다.

intelliJ 방식 QItem
app 실행 build 실행 

 

2. Querydsl 적용

  • 공통
    • Querydsl을 사용하려면 JPAQueryFactory가 필요
    • JPAQueryFactory는 JPA 쿼리인 JPQL을 만들기 때문에 EntityManager가 필요
    • 생성자 주입 방식은 JdbcTemplate 설정 방식과 유사

 

2.1. JpaItemRepositoryV3

  • save, update(), findById()는 JPA 기능 사용
package hello.itemservice.repository.jpa;

import com.querydsl.core.BooleanBuilder;
import com.querydsl.core.types.dsl.BooleanExpression;
import com.querydsl.jpa.impl.JPAQueryFactory;
import hello.itemservice.domain.Item;
import hello.itemservice.repository.ItemRepository;
import hello.itemservice.repository.ItemSearchCond;
import hello.itemservice.repository.ItemUpdateDto;
import org.springframework.stereotype.Repository;
import org.springframework.transaction.annotation.Transactional;
import org.springframework.util.StringUtils;

import javax.persistence.EntityManager;
import java.util.List;
import java.util.Optional;

import static hello.itemservice.domain.QItem.*;

@Repository
@Transactional
public class JpaItemRepositoryV3 implements ItemRepository {

    // jpa 사용하기 위한 bean 주입
    private final EntityManager em;
    private final JPAQueryFactory query;

    public JpaItemRepositoryV3(EntityManager em) {
        this.em = em;
        this.query = new JPAQueryFactory(em);
    }

    @Override
    public Item save(Item item) {
        em.persist(item);
        return item;
    }

    @Override
    public void update(Long itemId, ItemUpdateDto updateParam) {
        Item findItem = em.find(Item.class, itemId); // 1. 항상 select 수행
        findItem.setItemName(updateParam.getItemName());
        findItem.setPrice(updateParam.getPrice());
        findItem.setQuantity(updateParam.getQuantity());
    }

    @Override
    public Optional<Item> findById(Long id) {
        Item item = em.find(Item.class, id); // find(entity.class, pk)
        return Optional.ofNullable(item);
    }

    public List<Item> findAllOld(ItemSearchCond cond) {
        String itemName = cond.getItemName();
        Integer maxPrice = cond.getMaxPrice();

        // QItem item = new QItem("i"); // params가 alias
        // QItem item = QItem.item; // QItem.class가 item을 내부적으로 가지고 있음,

        BooleanBuilder builder = new BooleanBuilder();
        if (StringUtils.hasText(itemName)){
            builder.and(item.itemName.like("%" + itemName + "%"));
        }
        if (maxPrice != null) {
            builder.and(item.price.loe(maxPrice));
        }

        List<Item> result = query
                .select(item) // QItem static import로 뺌
                .from(item)
                .where(builder)
                .fetch();
        return result;
    }

    @Override
    public List<Item> findAll(ItemSearchCond cond) {
        String itemName = cond.getItemName();
        Integer maxPrice = cond.getMaxPrice();

        return query
                .select(item)
                .from(item)
                .where(likeItemName(itemName), maxPrice(maxPrice))
                .fetch();
    }

    private BooleanExpression likeItemName(String itemName){
        if (StringUtils.hasText(itemName)){
            return item.itemName.like("%" + itemName + "%");
        }
        return null;
    }
    private BooleanExpression maxPrice(Integer maxPrice){
        if (maxPrice != null) {
            return item.price.loe(maxPrice);
        }
        return null;
    }
}

 

2.2. 동적 쿼리

  • BooleanBuilder를 사용해서 where 조건을 넣어준다.
  • 최종적으로 JPAQueryFactory에 쿼리를 넣어준다.

2.2.1. findAllOld

  • method 내부에 다 넣는 방식
package hello.itemservice.repository.jpa;

import static hello.itemservice.domain.QItem.*;

@Repository
@Transactional
public class JpaItemRepositoryV3 implements ItemRepository {
    // jpa 사용하기 위한 bean 주입
    private final EntityManager em;
    private final JPAQueryFactory query;

    public JpaItemRepositoryV3(EntityManager em) {
        this.em = em;
        this.query = new JPAQueryFactory(em);
    }
    public List<Item> findAllOld(ItemSearchCond cond) {
        String itemName = cond.getItemName();
        Integer maxPrice = cond.getMaxPrice();

        // QItem item = new QItem("i"); // params가 alias
        // QItem item = QItem.item; // QItem.class가 item을 내부적으로 가지고 있음,

        BooleanBuilder builder = new BooleanBuilder();
        
        if (StringUtils.hasText(itemName)){
            builder.and(item.itemName.like("%" + itemName + "%"));
        }
        if (maxPrice != null) {
            builder.and(item.price.loe(maxPrice));
        }
        List<Item> result = query
                .select(item) // QItem static import로 뺌
                .from(item)
                .where(builder)
                .fetch();
        return result;
    }
}

 

2.2.2. findAll

  • private으로 복잡한 작업 빼준다. - 반환 타입이 BooleanExpression
  • 핵심 로직이 더 간결해진다.
package hello.itemservice.repository.jpa;

import static hello.itemservice.domain.QItem.*;

@Repository
@Transactional
public class JpaItemRepositoryV3 implements ItemRepository {
    // jpa 사용하기 위한 bean 주입
    private final EntityManager em;
    private final JPAQueryFactory query;

    public JpaItemRepositoryV3(EntityManager em) {
        this.em = em;
        this.query = new JPAQueryFactory(em);
    }
    
    @Override
    public List<Item> findAll(ItemSearchCond cond) {
        String itemName = cond.getItemName();
        Integer maxPrice = cond.getMaxPrice();
        return query
                .select(item)
                .from(item)
                .where(likeItemName(itemName), maxPrice(maxPrice))
                .fetch();
    }
    
    private BooleanExpression likeItemName(String itemName){
        if (StringUtils.hasText(itemName)){
            return item.itemName.like("%" + itemName + "%");
        }
        return null;
    }
    private BooleanExpression maxPrice(Integer maxPrice){
        if (maxPrice != null) {
            return item.price.loe(maxPrice);
        }
        return null;
    }
}