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가 제공하는 기본 기능 사용
- 기본 기능들은 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의 장점 !