프로젝트의 검색 기능에 QueryDSL을 적용해보기로 했다.
우리 프로젝트의 BookingService의 검색 메서드를 확인해보면
// 사용자, 공연명으로 예약 조회 null일 경우는 메서드에서 처리함
Page<Booking> bookingList = bookingRepository.findByBookingSearch(
userList.stream().map(GetUserRes::userId).toList(),
concertList.stream().map(GetConcertRes::id).toList(), BookingStatus.PENDING, pageable);
이런 부분이 있는데 여기서 Repository의 findByBookingSearch를 따라가보면
// 예매 대기 인 경우는 제외하고 예매 내역 조회
@Query("SELECT b FROM Booking b WHERE "
+ "(:userIds IS NULL OR b.userId IN :userIds) "
+ "AND (:concertIds IS NULL OR b.concertId IN :concertIds) "
+ "AND b.status != :status")
Page<Booking> findByBookingSearch(List<Long> userIds, List<Long> concertIds, BookingStatus status, Pageable pageable);
팀원이 작성해둔 쿼리문이 작성되어있다.
검색 조건이 까다로운 이 코드를 개선할 것이다.
가장 먼저 의존성을 추가한다.
// queryDSL
implementation 'com.querydsl:querydsl-jpa'
annotationProcessor 'com.querydsl:querydsl-apt:jpa'
annotationProcessor "jakarta.annotation:jakarta.annotation-api"
annotationProcessor "jakarta.persistence:jakarta.persistence-api"
Entity에 대한 Q-타입 클래스를 생성해야한다.
Q-타입 클래스는 QueryDSL을 사용하여 JPQL 또는 SQL 쿼리를 작성할 때 타입 안전성을 제공하는 클래스이다.
인텔리제이 우측 탭에서 Gradle을 눌러
Tasks - build - clean
Tasks - other - compileJava
빌드를 실행하면 각 서비스의 build 경로에 Q-타입 클래스와
BaseEntity 클래스가 있는 경로에 QBaseEntity가 ignore처리되면서 자동으로 생성된다.
QBooking은 QueryDSL을 사용할 때 쿼리 작성에 필요한 엔티티의 메타 데이터를 제공하는 클래스이다.
QueryDSL에서는 엔티티 클래스(Booking)를 직접 참조하지 않고 자동으로 생성된 Q 클래스를 사용해 쿼리를 작성한다.
이제 QueryDSL문을 작성하면 되는데
그 전에 JPAQueryFactory의 Bean을 관리하기 위한 QuerydslConfig를 작성해준다.
@Configuration
public class QuerydslConfig {
@PersistenceContext
private EntityManager entityManager;
@Bean
public JPAQueryFactory jpaQueryFactory() {
return new JPAQueryFactory(entityManager);
}
}
쿼리문을 작성하기 위한 Custom Interface를 작성
public interface BookingRepositoryCustom {
Page<Booking> findByBookingSearch(List<Long> userIds, List<Long> concertIds, Pageable pageable);
}
BookingRepositoryCustom을 implements하는 쿼리문을 작성한다.
@RequiredArgsConstructor
public class BookingRepositoryImpl implements BookingRepositoryCustom{
private final JPAQueryFactory queryFactory;
@Override
public Page<Booking> findByBookingSearch(List<Long> userIds, List<Long> concertIds, Pageable pageable) {
// Booking Entity에 대응되는 QBooking 객체를 통해 매핑하여 필드에 접근
QBooking booking = QBooking.booking;
// BooleanExpression : QueryDSL에서 조건을 표현하는 객체, where()절에 사용된다.
// userIds 리스트가 존재할 경우 booking.userId가 userIds에 포함된 데이터만 필터링
BooleanExpression userCondition = userIds != null && !userIds.isEmpty() ? booking.userId.in(userIds) : null;
// concertIds 리스트가 존재할 경우 booking.concertId가 concertIds에 포함된 데이터만 필터링
BooleanExpression concertCondition = concertIds != null && !concertIds.isEmpty() ? booking.concertId.in(concertIds) : null;
// 전체 예약을 조회할 조건
boolean isUserConditionEmpty = userCondition == null;
boolean isConcertConditionEmpty = concertCondition == null;
List<Booking> results;
if (isUserConditionEmpty && isConcertConditionEmpty) {
// nickname과 concertName이 모두 비어있을 때: 전체 예약 조회
results = queryFactory.selectFrom(booking)
.where(booking.status.ne(BookingStatus.PENDING)) // BookingStatus가 PENDING은 제외
.offset(pageable.getOffset())
.limit(pageable.getPageSize())
.fetch();
} else if (isUserConditionEmpty) {
// nickname만 비어있고 concertName이 있을 때: concertName에 해당하는 조건만 조회
results = queryFactory.selectFrom(booking)
.where(concertCondition, booking.status.ne(BookingStatus.PENDING)) // BookingStatus가 PENDING은 제외
.offset(pageable.getOffset())
.limit(pageable.getPageSize())
.fetch();
} else if (isConcertConditionEmpty) {
// concertName만 비어있고 nickname이 있을 때: nickname에 해당하는 조건만 조회
results = queryFactory.selectFrom(booking)
.where(userCondition, booking.status.ne(BookingStatus.PENDING)) // BookingStatus가 PENDING은 제외
.offset(pageable.getOffset())
.limit(pageable.getPageSize())
.fetch();
} else {
// nickname과 concertName 둘 다 있을 때: 두 조건에 해당하는 예약 조회
results = queryFactory.selectFrom(booking)
.where(userCondition, concertCondition, booking.status.ne(BookingStatus.PENDING)) // BookingStatus가 PENDING은 제외
.offset(pageable.getOffset())
.limit(pageable.getPageSize())
.fetch();
}
// 쿼리의 총 결과 수
long total = queryFactory.selectFrom(booking)
.where(userCondition, concertCondition, booking.status.ne(BookingStatus.PENDING))
.fetch().size();
return new PageImpl<>(results, pageable, total);
}
}
이제 BookingRepository로 와서 BookingRepositoryCustom를 extends하고 원래 있던 쿼리를 지운다.
public interface BookingRepository extends JpaRepository<Booking, Long>, BookingRepositoryCustom {
// 예매 대기 인 경우는 제외하고 예매 내역 조회
// @Query("SELECT b FROM Booking b WHERE "
// + "(:userIds IS NULL OR b.userId IN :userIds) "
// + "AND (:concertIds IS NULL OR b.concertId IN :concertIds) "
// + "AND b.status != :status")
// Page<Booking> findByBookingSearch(List<Long> userIds, List<Long> concertIds, BookingStatus status, Pageable pageable);
}