implementation 'io.github.openfeign.querydsl:querydsl-jpa:6.11'
annotationProcessor 'io.github.openfeign.querydsl:querydsl-apt:6.11:jpa'
annotationProcessor 'jakarta.annotation:jakarta.annotation-api'
annotationProcessor 'jakarta.persistence:jakarta.persistence-api'
Querydsl은 2024년 부로 업데이트를 중단하여, SQL 인젝션과 관련한 취약점이 해결되지 않고있다.
다행히도, OpenFeign 팀에서 Querydsl을 지속적으로 업데이트 중이므로 해당 의존성을 가져다 쓰면 된다. (기존의 Querydsl과 사용방법 동일)
package com.team1.monew.config;
import com.querydsl.jpa.impl.JPAQueryFactory;
import jakarta.persistence.EntityManager;
import jakarta.persistence.PersistenceContext;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
@Configuration
public class QueryDslConfig {
@PersistenceContext
private EntityManager entityManager;
@Bean
public JPAQueryFactory jpaQueryFactory() {
return new JPAQueryFactory(entityManager);
}
}
QInterest interest = QInterest.interest;
queryFactory.selectFrom(interest)
.where(interest.name.contains("운동"))
.fetch();
repository
public interface InterestRepository extends JpaRepository<Interest, Long>, InterestRepositoryCustom {
@Query("SELECT i FROM Interest i LEFT JOIN FETCH i.keywords WHERE i.id = :id")
Optional<Interest> findByIdFetch(@Param("id") Long id);
@Query("SELECT i FROM Interest i LEFT JOIN FETCH i.keywords")
List<Interest> findAllWithKeywords();
}
repositoryCustom
public interface InterestRepositoryCustom {
CursorPageResponse<InterestDto> searchByCondition(InterestSearchCondition condition);
}
repositoryCustomImpl
package com.team1.monew.interest.repository;
import com.querydsl.core.BooleanBuilder;
import com.querydsl.core.types.Order;
import com.querydsl.core.types.OrderSpecifier;
import com.querydsl.jpa.impl.JPAQueryFactory;
import com.team1.monew.interest.dto.InterestSearchCondition;
import com.team1.monew.interest.entity.Interest;
import com.team1.monew.interest.entity.QInterest;
import java.util.List;
import lombok.RequiredArgsConstructor;
import org.springframework.data.domain.Slice;
import org.springframework.data.domain.SliceImpl;
import org.springframework.stereotype.Repository;
import org.springframework.util.StringUtils;
@Repository
@RequiredArgsConstructor
public class InterestRepositoryCustomImpl implements InterestRepositoryCustom {
private final JPAQueryFactory queryFactory;
@Override
public Slice<Interest> searchByCondition(InterestSearchCondition condition) {
QInterest interest = QInterest.interest;
BooleanBuilder where = new BooleanBuilder();
if (StringUtils.hasText(condition.keyword())) {
where.and(
interest.name.containsIgnoreCase(condition.keyword())
.or(interest.keywords.any().keyword.containsIgnoreCase(condition.keyword()))
);
}
if (StringUtils.hasText(condition.cursor())) {
if ("subscriberCount".equalsIgnoreCase(condition.orderBy())) {
Long cursorValue = Long.parseLong(condition.cursor());
where.and("ASC".equalsIgnoreCase(condition.direction())
? interest.subscriberCount.gt(cursorValue)
: interest.subscriberCount.lt(cursorValue));
} else {
String cursorValue = condition.cursor();
where.and("ASC".equalsIgnoreCase(condition.direction())
? interest.name.gt(cursorValue)
: interest.name.lt(cursorValue));
}
}
Order direction = "ASC".equalsIgnoreCase(condition.direction()) ? Order.ASC : Order.DESC;
OrderSpecifier<?>[] orderSpecifiers = "subscriberCount".equalsIgnoreCase(condition.orderBy())
? new OrderSpecifier[]{ new OrderSpecifier<>(direction, interest.subscriberCount) }
: new OrderSpecifier[]{ new OrderSpecifier<>(direction, interest.name) };
List<Interest> results = queryFactory
.selectFrom(interest)
.leftJoin(interest.keywords).fetchJoin()
.where(where)
.orderBy(orderSpecifiers)
.limit(condition.limit() + 1)
.fetch();
boolean hasNext = results.size() > condition.limit();
List<Interest> content = hasNext ? results.subList(0, condition.limit()) : results;
return new SliceImpl<>(content, condition.toPageable(), hasNext);
}
}
}
=> 이렇게 반환받은 data를 Service, Controller단을 거치면서 커서 페이지네이션을 적용시키면 된다.