스프링/6. 스프링 DB-2

36. Querydsl 적용

sdafdq 2023. 10. 13. 10:57

자, Querydsl을 설정하면 QItem 이라는 클래스가 자동으로 만들어 졌을 것이다.

 

이제 그냥 그거 쓰면 된다.

 

이제 쿼리는

JPA, 즉 EntityManager를 통해 간단하게 사용.

Querydsl, 조건, 즉 쿼리를 동적으로 변경해야 할 경우 마치 블록 넣듯이 쿼리를 동적으로 변경하여 사용하고 싶을 떄 사용.

JdbcTemplate, 위의 저런 것 보다 그냥 쿼리 한번으로 짜는 게 훨씬 깔끔할 경우, 혹은 JPA나 Querydsl로 해결하지 못한 경우

 

이렇게 3가지 경우로 쓴다.

주는 JPA, Querydsl을 쓰다가, (이 중에서도 주는 JPA이고 동적으로 쿼리를 만들어야 할 경우에 Querydsl)

가끔 쿼리를 직접 작성해야 할 경우 Jdbc템플릿을 쓰는 느낌이다.

 

일단은, 자동으로 생성된 QItem

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

}

뭐랄까 보면 동적쿼리를 생성하는데 참고할 Item의 속성? columns를 정리해 놓은 걸 생성해 놓은 느낌.

그러니까, 아마 어떤 Querydsl에서 사용하는 양식(클래스)에 맞춰서 자동으로 생성되게끔 해놓은 듯.

 

그럼 본격적으로 JPA + Querydsl의 합작

@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 findedItem = em.find(Item.class, itemId);
        findedItem.setItemName(updateParam.getItemName());
        findedItem.setPrice(updateParam.getPrice());
        findedItem.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));
        }

        return query.select(QItem.item)
                .from(QItem.item)
                .where(builder)
                .fetch();
    }
}

코드가 짧아졌다, 아니 그것도 있지만 보다 코드가 직관적이게 되었다.

 

먼저 save

JPA, EntityManager, 도메인을 @Entity로 등록해 사용한다고 해서 EntityManager 같은데, 여튼 저렇게 em.persist(item) 하면 저장 됨. 그러면 우리가 전에 저 Item 도메인에

 

@Data
@Entity
//@Table(name="item")
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;
    }
}

이렇게 @Id 붙여놔서, 자동으로 DB에서 할당한 id를 가져와서 저 객체에다가 넣어줌.

그래서 그냥 item 반환하면 id까지 같이 반환되서 완전한 Item을 반환받을 수 있음.

 

그 다음 update

저거는 그냥 em.find(어떤클래스인지, id)

넣어서 찾아온 다음.

객체 값 바꾸듯 바꾸면 됨.

그럼 실제로 저 JPA 내부에서 원래 모습을 캡쳐해놨다가, 저 서비스가 끝날 때 commit하기 전 바뀐 모습을 보고 쿼리를 만들어서 날린 후 commit함. 이 부분에 대한 자세한 것은 JPA 배울 때 배울 듯.

 

그 다음 findById

그냥 em.find(어떤클래스인지, id) 해서 찾아오면 됨.

null일수도 있으니,

Optional로 감싸서 반환토록 해놔서,

Optional.ofNullable(item) 해서 반환.

 

다음, 이번 Querydsl이 들어간 findAll

조건에 따라 쿼리가 달라지는 동적 쿼리

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

    return query.select(QItem.item)
            .from(QItem.item)
            .where(builder)
            .fetch();
}

 

먼저, 조건이 되는 itemName과 maxPrice를 뽑아옴.

QItem을 짧게 쓰기 위해 그냥 item으로 가져 옴. 위에 QItem 클래스 보면, 저렇게 자기 자신 생성해서 static으로 가지고 있음. 그냥 뭐랄까 싱글톤 비슷하게 보면 편하려나?

QItem item = QItem.item 하긴 했는데,

QItem.item 자체가 static이라 add on 뭐시기 였나 그거 하면 QItem 없이 쓸 수 있음.

그래서 QItem item = QItem.item 해서 짧게 할 필요도 없음.

 

여튼,

BooleanBuilder는 이게 참이냐 거짓이냐에 따라 값을 줄지 말지가 결정되는..?

아니 그거 보다는 null이면 안넣고 있으면 넣고 그거임.

여튼, builder.and(item.itemName.like("%" + itemName + "%"));

해서 and문을 추가할 수 있음

builder.and(item.itemName.like("%" + itemName + "%"));

빌드.앤드 아이템의 아이템이름의 조건(like)이 ~~~~

 

다음 and문도

builder.and(item.price.loe(maxPrice));

빌드.앤드를 추가한다 내용은

아이템의 가격이 조건(Less Or Equal)이고 기준

and(항목.조건.기준)

 

그 다음

private final JPAQueryFactory query;

이게 QuerydslJPA다. 

query.select(QItem.item)
            .from(QItem.item)
            .where(builder)
            .fetch();

 

보면 저기 파란거가 함수로 쓰는 것 같다.

쿼리시작.select문
            .from(아이템정보)
            .where(조건)
            .fetch();

그러니까, builder에 조건에 따라 쿼리를 추가 하다가,

where 에 and 하면서 쿼리 넣으니까 where에다가 넣으면 된다.

fetch는 가져오는 거고.

 

 

근데, 저것보다 더 깔끔한 게 있다.

 

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

    return query.select(QItem.item)
            .from(QItem.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;
}

이렇게 할 수 있다.

BooleanExpression

직역하면 참거짓 표현식인데,

참거짓에 따라 표현식을 넣을 지 말지 결정한다는 것인데,

그것보다는 null인지 아닌지에 따라 넣을 지 말지 결정하는 것 이다.

여튼 저 반환값으로 하면 null이면 넣지 않고,

null이 아닌 뭔가 표현식이 있으면 넣는다.

저렇게 추가시켜 주면, 다 and로 추가 된다. (여러 개 넣을 수 있음)

그래서 보면 return할 때 따로 and 쓴 게 없다.

 

그리고 이렇게 하면 좋은 게,

저 쿼리조건이 모듈화가 된다.

 

물론 근데, 저 둘 방법 모두 많이 씀.

그냥 상황에 따라 더 괜찮은 방법으로 사용하면 될 듯.

뭔가 재사용 할 가능성이 있으면 메소드로 뽑아두고, 아니면 그냥 저기에서만 딱 한정되게 메소드로 뽑지 말고

아니면 또 저 두개가 거의 짝꿍이면 BooleanBuilder를 생성해서 한 메소드에 같이 쓰던.

'스프링 > 6. 스프링 DB-2' 카테고리의 다른 글

38. 실용적인 구조로 변환  (0) 2023.10.14
37. 트레이드 오프, SpringDataJPA 사용에 따른 구조변화  (0) 2023.10.13
35. Querydsl 설정  (0) 2023.10.13
34. Querydsl 등장  (0) 2023.10.13
33. Querydsl  (0) 2023.10.13