QueryDSL
Spring Data Jpa의 한계
Spring Data Jpa를 통해 Jpa를 더욱 더 편리하게 사용할 수 있었다.
하지만 여전히 해결되지 않는 두 가지 문제가 있다.
그 두 가지 문제는 다음과 같다.
1. 동적쿼리 문제
@Override
public List<Item> findAll(ItemSearchCond cond) {
String itemName = cond.getItemName();
Integer maxPrice = cond.getMaxPrice();
if(StringUtils.hasText(itemName) && maxPrice != null) {
// return repository.findByItemNameLikeAndPriceLessThanEqual(itemName, maxPrice);
return repository.findItems("%" + itemName + "%", maxPrice);
} else if(StringUtils.hasText(itemName)) {
return repository.findByItemNameLike("%" + itemName + "%");
} else if (maxPrice != null) {
return repository.findByPriceLessThanEqual(maxPrice);
} else {
return repository.findAll();
}
}
여전히 복잡한 것을 알 수 있다.
2. 쿼리 오류 검출이 불가능
지금까지 모든 쿼리는 문자열로 작성되었다.
예시:
// 쿼리 직접 실행
@Query("select i from Item i where i.itemName like :itemName and i.price <= :price")
List<Item> findItems(@Param("itemName") String itemName, @Param("price") Integer price);
그렇기 때문에 쿼리에 문제가 생기면 컴파일 오류로 검출할 수가 없다.
이는 쿼리가 실행되기 전까지 오류가 발견되지 않는다는 소리이다. 이는 개발자가 놓치기 쉬운 치명적인 오류이다.
빌드에 아무 문제가 없어 배포했는데 막상 사용자가 서비스를 이용하다 오류가 발생하면 이는 상당히 큰 문제가 된다.
QueryDSL
이 두 가지 문제점을 해결해주는 것이 바로 QueryDSL이다.
QueryDSL은 Java로 type-safe하게 쿼리를 작성할 수 있게 해주는 프레임워크이다.
QueryDSL을 사용하면 자바코드로 쿼리를 작성해 오류가 발생해도 컴파일오류가 발생한다.
QueryDSL은 Q타입이라는 별도의 클래스를 생성한다.
이 Q타입 클래스는 @Entity가 붙은 엔티티 객체들에 대해 생성된다.
Q타입 예시: QMember.java
@Generated("com.mysema.query.codegen.EntitySerializer")
public class QMember extends EntityPathBase<Member>{
public final NumberPath<Long> id = createNumber("id",Long.class);
public final NumberPath<Integer> age = createNumber("age",Integer.class);
public final String Pathname = createString("name");
public static final QMember member=newQMember("member");
...
}
이제 본격적인 사용법을 알아보자.
QueryDSL 사용하기
1. 아이템 도메인(Item)
@Data
@Entity
public class Item {
@Id @GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
private String itemName;
private Integer price;
private Integer quantity;
public Item() {
}
public Item(String itemName, Integer price, Integer quantity) {
this.itemName = itemName;
this.price = price;
this.quantity = quantity;
}
}
2. 아이템 리포지토리(ItemRepository)
public interface ItemRepository {
Item save(Item item);
void update(Long itemId, ItemUpdateDto updateParam);
Optional<Item> findById(Long id);
List<Item> findAll(ItemSearchCond cond);
}
- Item save(Item item): 아이템 객체를 데이터베이스에 저장
- void update(Long itemId, ItemUpdateDto updateParam): 아이템 객체를 id를 통해 업데이트
- Optional<Item> findById(Long id): id를 통해 아이템 객체 찾기
- List<Item> findAll(ItemSearchCond cond): 특정 조건의 Item 모두찾기
3. 아이템 검색 조건(ItemSearchCond)
@Data
public class ItemSearchCond {
private String itemName;
private Integer maxPrice;
public ItemSearchCond() {
}
public ItemSearchCond(String itemName, Integer maxPrice) {
this.itemName = itemName;
this.maxPrice = maxPrice;
}
}
설정하기
1) build.gradle
// ⭐ Spring boot 3.x이상에서 QueryDsl 패키지를 정의하는 방법
implementation 'com.querydsl:querydsl-jpa:5.0.0:jakarta'
annotationProcessor "com.querydsl:querydsl-apt:5.0.0:jakarta"
annotationProcessor "jakarta.annotation:jakarta.annotation-api"
annotationProcessor "jakarta.persistence:jakarta.persistence-api"
출처: https://velog.io/@juhyeon1114/Spring-QueryDsl-gradle-%EC%84%A4%EC%A0%95-Spring-boot-3.0-%EC%9D%B4%EC%83%81
2) Q타입 생성하기
1. Gradle - other - compileJava 클릭
2. build>generated>sources>annotationProcessor>java>...>QItem 생성 확인
3) Q타입 삭제하기
gradle>build>clean 클릭
Q 타입
Q타입은 QueryDSL의 핵심 개념이다.
@Entity가 붙은 엔티티 객체이름 앞에 Q가 붙은 이름을 가진다.(Item.java --> QItem.java)
Item.java
@Data
@Entity
public class Item {
@Id @GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
@Column(name = "item_name", length = 10)
private String itemName;
private Integer price;
private Integer quantity;
public Item() {
}
public Item(String itemName, Integer price, Integer quantity) {
this.itemName = itemName;
this.price = price;
this.quantity = quantity;
}
}
QItem.java
/**
* QItem is a Querydsl query type for Item
*/
@Generated("com.querydsl.codegen.DefaultEntitySerializer")
public class QItem extends EntityPathBase<Item> {
private static final long serialVersionUID = -570080939L;
public static final QItem item = new QItem("item");
public final NumberPath<Long> id = createNumber("id", Long.class);
public final StringPath itemName = createString("itemName");
public final NumberPath<Integer> price = createNumber("price", Integer.class);
public final NumberPath<Integer> quantity = createNumber("quantity", Integer.class);
public QItem(String variable) {
super(Item.class, forVariable(variable));
}
public QItem(Path<? extends Item> path) {
super(path.getType(), path.getMetadata());
}
public QItem(PathMetadata metadata) {
super(Item.class, metadata);
}
}
리포지토리
@Repository
@Transactional
public class JpaItemRepositoryV4 implements ItemRepository {
private final SpringDataJpaItemRepository repository;
private final JPAQueryFactory query;
public JpaItemRepositoryV4(SpringDataJpaItemRepository repository, EntityManager em) {
this.repository = repository;
this.query = new JPAQueryFactory(em);
}
@Override
public Item save(Item item) {
repository.save(item);
return item;
}
@Override
public void update(Long itemId, ItemUpdateDto updateParam) {
Item findItem = repository.findById(itemId).orElseThrow();
findItem.setItemName(updateParam.getItemName());
findItem.setPrice(updateParam.getPrice());
findItem.setQuantity(updateParam.getQuantity());
}
@Override
public Optional<Item> findById(Long id) {
return repository.findById(id);
}
public List<Item> findAllOld(ItemSearchCond cond) {
String itemName = cond.getItemName();
Integer maxPrice = cond.getMaxPrice();
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();
BooleanBuilder builder = new BooleanBuilder();
return query.select(item)
.from(item)
.where(likeItemName(itemName), maxPrice(maxPrice))
.fetch();
}
private BooleanExpression maxPrice(Integer maxPrice) {
if (maxPrice != null) {
return item.price.loe(maxPrice);
}
return null;
}
private BooleanExpression likeItemName(String itemName) {
if (StringUtils.hasText(itemName)) {
return item.itemName.like("%" + itemName + "%");
}
return null;
}
}
1. JpaQueryFactory
private final SpringDataJpaItemRepository repository;
private final JPAQueryFactory query;
public JpaItemRepositoryV4(SpringDataJpaItemRepository repository, EntityManager em) {
this.repository = repository;
this.query = new JPAQueryFactory(em);
}
- QueryDSL을 사용하려면 JpaQueryFactory를 사용해야 한다.
- JpaQueryFactory는 인자로 EntityManager를 가진다.
2. findAllOld()
public List<Item> findAllOld(ItemSearchCond cond) {
String itemName = cond.getItemName();
Integer maxPrice = cond.getMaxPrice();
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;
}
- QItem item
- Q타입을 보면 자기 자신의 정적 객체를 하나 갖고 있는 것을 알 수 있다.
- 이 객체를 사용해 쿼리를 작성할 수 있다.
- BooleanBuilder
- 쿼리의 where 조건을 넣어주는 클래스이다.
- where의 인자로 넣어주면된다.
- query.select()
- select 쿼리를 사용하고 싶을 때 사용한다.
- from(): 엔티티 객체
- where(): 조건
- fetch(): 결과를 받아옴
3. findAll() (findAllOld() 리팩토링)
@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 maxPrice(Integer maxPrice) {
if (maxPrice != null) {
return item.price.loe(maxPrice);
}
return null;
}
private BooleanExpression likeItemName(String itemName) {
if (StringUtils.hasText(itemName)) {
return item.itemName.like("%" + itemName + "%");
}
return null;
}
- BooleanExpression
- 조건에 대한 참과 거짓을 담는 클래스이다.
- 거짓이면 null이 들어간다.
- 위와 같이 콤마(,)로 BooleanExpression을 나열하면 And 조건이 된다.
- or로 하고 싶다면 where(BooleanExpression.or(BooleanExpression))
정리
1. QueryDSL: 자바에서 쿼리를 Type-Safe하게 작성할 수 있게 해줌(쿼리 잘못 작성하면 컴파일 오류 내줌)
2. Q타입: @Entity가 붙어있는 엔티티 객체를 보고 자동으로 생성해주는 타입. 이걸 가지고 쿼리를 작성함
3. JpaQueryFactory: QueryDSL에서 쿼리 관련 메서드를 사용하기 위한 클래스. 인자로 EntityManager를 받음
4. BooleanBuilder, BooleanExpression: where조건 넣을 때 사용
해당 포스트는 김영한 님의 스프링 DB 2편 - 데이터 접근 활용 기술 강의를 기반으로 작성되었습니다.
출처: https://www.inflearn.com/course/%EC%8A%A4%ED%94%84%EB%A7%81-db-2
'Spring > Spring Data' 카테고리의 다른 글
[Spring Data] Spring Transaction의 트랜잭션 전파 (0) | 2024.06.09 |
---|---|
[Spring Data] Spring Transaction의 이해 (0) | 2024.06.08 |
[Spring Data] Spring Data JPA (0) | 2024.06.07 |
[Spring Data] JPA (0) | 2024.06.07 |
[Spring Data] MyBatis (0) | 2024.06.06 |