Spring

[인프런/스프링 DB 2편] 4. 데이터 접근 기술 (5) Query DSL

주니어주니 2023. 7. 17. 12:40

 

 

1. Query DSL 

 

  • DSL (도메인 특화 언어)
  • QueryDSL : 쿼리에 특화된 프로그래밍 언어
  • 원래 쿼리 작성 -> 실행해야 오류를 알 수 있음 (컴파일X, 런타임 에러)
  • Query DSL -> 주로 JPA(JPQL) 같은 기술을 위해 쿼리를 자바코드처럼 type-safe (컴파일시 에러체크) 하게 개발할 수 있게 지원하는 프레임워크
  • JPA의 JPQL을 만들어주는 빌더 역할
    (QueryDSL -> JPQL -> SQL)

 

 

2. Query DSL 적용 

 

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'

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'

	//JdbcTemplate 추가
//	implementation 'org.springframework.boot:spring-boot-starter-jdbc'
	//MyBatis 추가
	implementation 'org.mybatis.spring.boot:mybatis-spring-boot-starter:2.2.0'
	//JPA, 스프링 데이터 JPA 추가
	implementation 'org.springframework.boot:spring-boot-starter-data-jpa'

	//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"

	//H2 데이터베이스 추가
	runtimeOnly 'com.h2database:h2'
	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'
}

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

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

 

 

2) Q타입 생성

 

(1) Gradle

  • Gradle -> Tasks -> build -> clean 
  • Gradle -> Tasks -> other -> compileJava
  • Q 타입 객체 생성 확인
    build -> generated -> sources -> annotationProcessor -> java/main -> hello.itemservice.domain.QItem 

 

(2) IntelliJ IDEA 

  • main() 실행 
  • Q 타입 객체 생성 확인
    src/main/generated -> hello.itemservice.domain.QItem

 

 

3) Repositocy 

package hello.itemservice.repository.jpa;

import com.querydsl.core.BooleanBuilder;
import com.querydsl.core.types.Predicate;
import com.querydsl.core.types.dsl.BooleanExpression;
import com.querydsl.jpa.impl.JPAQueryFactory;
import hello.itemservice.domain.Item;
import hello.itemservice.domain.QItem;
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 {

    private final EntityManager em;		// 간단한 JPA 실행 (쿼리 만들어서 실행)
    private final JPAQueryFactory query;	// 동적쿼리를 위한 QueryDSL을 사용하기 위함

    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);
        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);
        return Optional.ofNullable(item);
    }

    public List<Item> findAllOld(ItemSearchCond cond) {

        String itemName = cond.getItemName();
        Integer maxPrice = cond.getMaxPrice();

        QItem item = QItem.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)
                .from(item)
                .where(builder)
                .fetch();

        return result;
    }

    @Override
    public List<Item> findAll(ItemSearchCond cond) {

        String itemName = cond.getItemName();
        Integer maxPrice = cond.getMaxPrice();

        List<Item> result = query
                .select(item)
                .from(item)
                .where(LikeItemName(itemName), maxPrice(maxPrice))
                .fetch();

        return result;
    }

    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;
    }

}

 

  • Querydsl 사용
    • JPAQueryFactory : 동적 쿼리 작성을 위한 QueryDSL 
    • EntityManager : 간단한 JPA 기능 사용

  • save(), update(), findById()
    • 기본 기능들은 JPA가 제공하는 기본 기능 사용

  • findAllOld
    • QueryDSL을 사용해서 동적 쿼리 문제 해결
    • Q 객체와 BooleanBuilder를 사용해서 where 조건들을 넣어줌

  • findAll
    • findAllOld 코드 리팩토링
    • where(A, B)에 들어간 조건들은 AND 조건으로 처리, null이 들어가면 해당 조건 무시
    • 다른 쿼리를 작성할 때 재사용 가능

 

 

4) Config

package hello.itemservice.config;

import hello.itemservice.repository.ItemRepository;
import hello.itemservice.repository.jpa.JpaItemRepositoryV3;
import hello.itemservice.service.ItemService;
import hello.itemservice.service.ItemServiceV1;
import lombok.RequiredArgsConstructor;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

import javax.persistence.EntityManager;

@Configuration
@RequiredArgsConstructor
public class QuerydslConfig {

    private final EntityManager em;

    @Bean
    public ItemService itemService() {
        return new ItemServiceV1(itemRepository());
    }
    @Bean
    public ItemRepository itemRepository() {
        return new JpaItemRepositoryV3(em);
    }
}

 

 

5) Application

@Slf4j
@Import(QuerydslConfig.class)
@SpringBootApplication(scanBasePackages = "hello.itemservice.web")
public class ItemServiceApplication { }

 

 

6) QueryDSL 장점 

  • 동적 쿼리를 깔끔하게 작성
  • 쿼리 문장에 오타가 있을 경우, 컴파일 시점에 수정 가능
  • 메소드 추출을 통해 코드 재사용 가능
  • --> 쿼리를 자바코드로 작성하는 QureyDSL의 장점 !