기존 query의 문제점
String sql = "select * from member" +
"where name like ?" +
"and age between ? and ?" // 공백이 없어 오류
// = select * from memberwhere name like ?and age between ? and ?"
⚽ 만약 SQL이 클래스처럼 타입이 있고 자바 코드로 작성할 수 있다면 type-safe이다.
Query(쿼리) + Domain(도메인) + Specific(특화) + Language(언어)
QueryDSL은 쿼리를 JAVA로 type-safe하게 개발할 수 있게 지원하는 프레임워크이다. 주로 JPA쿼리(JPQL)에 사용한다.
build.gradle
QueryDSL로 추가된 부분
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"
}
"com.querydsl:querydsl-apt:${dependencyManagement.importedProperties['querydsl.version']}:jpa"
은 한줄로 붙어있는코드이다.
//Querydsl 추가, 자동 생성된 Q클래스 gradle clean으로 제거
clean {
delete file('src/main/generated')
}
Preferences → Build, Execution, Deployment → Build Tools → Gradle
여기에 가면 크게 2가지 옵션을 선택할 수 있다. 참고로 옵션은 둘다 같게 맞추어 두자.
1. Gradle: Gradle을 통해서 빌드한다.
2. IntelliJ IDEA: IntelliJ가 직접 자바를 실행해서 빌드한다.
Gradle IntelliJ 사용법
Q 타입 생성 확인
build -> generated -> sources -> annotationProcessor -> java/main 하위에 hello.itemservice.domain.QItem
이 생성되어 있어야 한다.
참고: Q타입은 컴파일 시점에 자동 생성되므로 버전관리(GIT)에 포함하지 않는 것이 좋다. gradle 옵션을 선택하면 Q타입은 gradle build 폴더 아래에 생성되기 때문에 여기를 포함하지 않아야 한다. 대부분 gradle build 폴더를 git에 포함하지 않기 때문에 이 부분은 자연스럽게 해결된다.
Q타입 삭제
gradle clean 을 수행하면 build 폴더 자체가 삭제된다. 따라서 별도의 설정은 없어도 된다.
Build -> Build Project 또는
Build -> Rebuild 또는
main() 또는 테스트를 실행하면 된다.
src/main/generated
하위에
hello.itemservice.domain.QItem
이 생성되어 있어야 한다
import static hello.itemservice.domain.QItem.*;
@Repository
@Transactional
public class JpaItemRepositoryV3 implements ItemRepository {
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 = findById(itemId).orElseThrow();
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 itemSearch) {
String itemName = itemSearch.getItemName();
Integer maxPrice = itemSearch.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;
}
}
⚽ 공통
EntityManager
가 필요하다.기본 기능들은 JPA가 제공하는 기본 기능을 사용한다.
⚽ findAllOld
BooleanBuilder
를 사용해서 원하는 where 조건들을 넣어주면 된다.⚽ findAll
앞서 findAllOld 에서 작성한 코드를 깔끔하게 리팩토링 했다. 다음 코드는 누가 봐도 쉽게 이해할 수 있을 것이다.
List<Item> result = query
.select(item)
.from(item)
.where(likeItemName(itemName), maxPrice(maxPrice))
.fetch();
Querydsl에서 where(A,B) 에 다양한 조건들을 직접 넣을 수 있는데, 이렇게 넣으면 AND 조건으로 처리된다. 참고로 where() 에 null 을 입력하면 해당 조건은 무시한다.
이 코드의 또 다른 장점은 likeItemName() , maxPrice() 를 다른 쿼리를 작성할 때 재사용 할 수 있다는 점이다. 쉽게 이야기해서 쿼리 조건을 부분적으로 모듈화 할 수 있다. 자바 코드로 개발하기 때문에 얻을 수 있는 큰 장점이다.
⚽ QuerydslConfig
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);
}
}
👻 Querydsl덕분에 동적 쿼리를 매우 깔끔하게 사용할 수 있다.
List<Item> result = query
.select(item)
.from(item)
.where(likeItemName(itemName), maxPrice(maxPrice))
.fetch();
쿼리 문장에 오타가 있어도 컴파일 시점에 오류를 막을 수 있다.
메서드 추출을 통해서 코드를 재사용할 수 있다. 예를 들어서 여기서 만든 likeItemName(itemName) , maxPrice(maxPrice) 메서드를 다른 쿼리에서도 함께 사용할 수 있다. Querydsl을 사용해서 자바 코드로 쿼리를 작성하는 장점을 느껴보았을 것이다. 그리고 동적 쿼리 문제도 깔끔하게 해결해보았다. Querydsl은 이 외에도 수 많은 편리한 기능을 제공한다. 예를 들어서 최적의 쿼리 결과를 만들기 위해서 DTO로 편리하게 조회하는 기능은 실무에서 자주 사용하는 기능이다.
JPA를 사용한다면 스프링 데이터 JPA와 Querydsl은 실무의 다양한 문제를 편리하게 해결하기 위해 선택하는 기본 기술이라 생각한다.