공부할 것 !

QueryDSL의 주요 기능 ( BooleanExpression, BooleanBuilder, OrderSpecifier)

sejin2 2024. 4. 21. 14:10

1. BooleanExpression 과 BooleanBuilder

위 두 기능은 QueryDSL에서 조건문을 구성할 때 사용한다. 

1-1. BooleanExpression 


BooleanExpression은 불변 객체로 한 번 생성되면 그 상태가 변경되지 않는다.
BooleanExpression은 개별 조건을 표현하며, 이러한 조건들을 연결하여 복잡한 쿼리를 구성할 수 있다.
비교, 논리, 연산, 수학적 조건 등 다양한 형태의 조건을 나타낼 수 있고, 새로운 BooleanExpression를 반환한다.

예시

BooleanExpression condition1 = qProduct.price.gt(100);
BooleanExpression condition2 = qProduct.category.eq("Books");
BooleanExpression combinedCondition = condition1.and(condition2);

combinedCondition은 새로운 BooleanExpression 객체이며, 원래의 condition1과 condition2는 변경되지 않는다.

1-2 BooleanBuilder

BooleanBuilder는 가변 객체로 하나 이상의 BooleanExpression을 논리적으로 조합하여 복잡한 조건을 동적으로 구성할 수 있다. 또 조건이 실행 도중에 추가되거나 변경될 수 있다.
BooleanExpression는 내부적으로 하나 이상의 BooleanExpression을 관리하면서, 필요에 따라 AND / OR 등의 논리 연산자로 연결하여 동적으로 조건을 구성한다. 
이는 동적 쿼리를 구성할 때 유용하며, 실행 시점에 조건을 추가하거나 변경하는 데에 사용된다. 

차이점

1. 변경가능성
- BooleanExpression : 불변 객체로, 새 조건이 필요할 때마다 새 객체를 생성한다.
- BooleanBuilder : 가변 객체로, 조건을 동적으로 추가하며 내부 상태가 변경된다.

2. 유연성
- BooleanExpression : 조건을 명시적으로 연결해야 하며, 복잡한 동적 구조 조건 구성에는 제한적이다.
- BooleanBuilder : 매우 유연하며, 실행 시점에 조건을 추가하거나 제거하는 것이 간단하다. 따라서 사용자의 입력과 같이 변화하는 조건에 특히 유용하다. 

3. 사용 케이스
- BooleanExpression : 조건이 상대적으로 고정적이거나, 조건의 구조가 간단할 때 적합하다.
- BooleanBuilder : 조건이 실행 중에 결정되거나 사용자의 입력과 같이 다양한 요소에 따라 달라질 때 적합하다. 

즉, BooleanExpression은 단일 조건, BooleanBuilder는 여러 BooleanExpression 을 조합하여 복합적인 조건을 만들 때 사용된다. BooleanExpression으로도 조건식을 사용할 수 있지만, BooleanBuilder는 이러한 조건들을 조합하여 다양하고 복잡한 쿼리를 작성할 수 있다. 

실제 프로젝트 때 사용한 예시

@Override
public List<Product> findProductListByFilterOptions(List<String> searchKeyword,
                                                    List<String> categoryNo,
                                                    List<String> subCategoryName, 
                                                    List<String> mainTypes,
                                                    List<String> types,
                                                    List<String> alcoholLevels,
                                                    List<String> prices) {

    QProduct qProduct = QProduct.product;
    QLikeProduct qLikeProduct = QLikeProduct.likeProduct;
    QReview qReview = QReview.review;
    BooleanBuilder finalBuilder = new BooleanBuilder();

    // 검색어 필터
    if (searchKeyword != null && !searchKeyword.isEmpty()) {
        BooleanBuilder keywordBuilder = new BooleanBuilder();
        searchKeyword.forEach(keyword -> {
            if (keyword != null) {
                keywordBuilder.or(qProduct.name.containsIgnoreCase(keyword));
            }
        });
        finalBuilder.and(keywordBuilder);
    }

    // 카테고리 번호 필터
    if (categoryNo != null && !categoryNo.isEmpty()) {
        BooleanBuilder categoryBuilder = new BooleanBuilder();
        try {
            Integer categoryNumber = categoryNo.get(0) != null ? Integer.valueOf(categoryNo.get(0)) : null;
            if (categoryNumber != null) {
                List<String> subCategories = subCategoryService.findCategoryNameByCategoryNo(categoryNumber);
                if (subCategories != null && !subCategories.isEmpty()) {
                    subCategories.forEach(subCategory -> {
                        if (subCategory != null) {
                            categoryBuilder.or(qProduct.keyword.contains(subCategory));
                        }
                    });
                } else {
                    // 서브 카테고리가 비어있는 경우, 주 카테고리만으로 검색 진행
                    categoryBuilder.or(qProduct.productId.eq(String.valueOf(categoryNumber)));
                }
                finalBuilder.and(categoryBuilder);
            }
        } catch (NumberFormatException e) {
            log.error("Invalid category number format", e);
        }
    }

    // 하위 카테고리 이름 필터
    if (subCategoryName != null && !subCategoryName.isEmpty()) {
        BooleanBuilder subCategoryBuilder = new BooleanBuilder();
        subCategoryName.forEach(keyword -> {
            if (keyword != null) {
                subCategoryBuilder.or(qProduct.keyword.contains(keyword));
            }
        });
        finalBuilder.and(subCategoryBuilder);
    } 

    // 주종 필터
    if (mainTypes != null && !mainTypes.isEmpty()) {
        BooleanBuilder mainTypeBuilder = new BooleanBuilder();
        mainTypes.forEach(mainType -> mainTypeBuilder.or(qProduct.productId.eq(mainType)));
        finalBuilder.and(mainTypeBuilder);
    }

    // 유형 필터
    if (types != null && !types.isEmpty()) {
        BooleanBuilder typeBuilder = new BooleanBuilder();
        types.forEach(type -> typeBuilder.or(qProduct.type.eq(type)));
        finalBuilder.and(typeBuilder);
    }

    // 알코올 도수 필터
    if (alcoholLevels != null && !alcoholLevels.isEmpty()) {
        BooleanBuilder alcoholLevelBuilder = new BooleanBuilder();
        alcoholLevels.forEach(alcoholLevel -> alcoholLevelBuilder.or(createAlcoholFilter(alcoholLevel)));
        finalBuilder.and(alcoholLevelBuilder);
    }

    // 가격 필터
    if (prices != null && !prices.isEmpty()) {
        BooleanBuilder priceRangeBuilder = new BooleanBuilder();
        prices.forEach(priceRange -> priceRangeBuilder.or(createPriceFilter(priceRange)));
        finalBuilder.and(priceRangeBuilder);
    }

    List<Product> productList = factory.select(qProduct)
            .from(qProduct)
            .where(finalBuilder)
            .fetch();

    return new ArrayList<>(productList);
}

 

2. OrderSpecifier

QueryDSL에서 정렬 순서를 지정할 때 사용되는 클래스이다. 
이를 통해 쿼르 결과의 정렬 방식을 제어할 수 있으며, 쿼리 내에서 정렬 순서를 지정할 수 있다.
OrderSpecifier를 사용하면 결과를 오름차순 또는 내림차순으로 정렬할 수 있으며, 다중 필드 정렬 등 복잡한 정렬 기준도 쉽게 구현할 수 있다.

사용방법
OrderSpecifier 객체를 생성할 때는 정렬 방향 ( Order.ASC 또는 Order.DESC )과 정렬할 필드를 지정해야 한다.
이 필드는 일반적으로 Q 클래스 속성을 사용한다.

예시

QUser user = QUser.user; 

// 이름으로 오름차순 정렬
OrderSpecifier<String> orderByName = new OrderSpecifier<>(Order.ASC, user.name);

// 나이로 내림차순 정렬 (이름이 같을 경우)
OrderSpecifier<Integer> orderByAgeDesc = new OrderSpecifier<>(Order.DESC, user.age);

List<User> users = new JPAQuery<>(entityManager)
    .select(user)
    .from(user)
    .where(user.isActive.isTrue())
    .orderBy(orderByName, orderByAgeDesc)
    .fetch();

추가 기능
- null처리 : QueryDSL은 null 값을 어떻게 처리할 지 지정할 수 있는 옵션을 제공한다. 
nullsFirst( ) 또는 nullsLast( )를 사용하여 정렬시 null 값을 처음이나 마지막에 위치하도록 할 수 있다. 
- 사용자 정의 비교자 : 사용자가 직접 비교 로직을 정의하여 정렬 기준으로 사용할 수도 있다.

 

'공부할 것 !' 카테고리의 다른 글

HTTP 메서드의 이해와 활용  (0) 2024.05.12
NoSQL  (0) 2024.05.08
아스키 코드  (0) 2024.05.08
사용자 정보 저장 및 사용 방법 ( 쿠키, 세션, 스프링시큐리티 )  (0) 2024.04.21
[JPA] QueryDSL  (0) 2024.04.13