์ด ๊ธ์์๋ ๋์ ์ผ๋ก ์กฐ๊ฑด๊ณผ ์ ๋ ฌ์ ๋ฐ์ํ๋ ๋์ ์ฟผ๋ฆฌ์ ๋์ ์ ๋ ฌ์ ์ํ
Querydsl4RepositorySupportํด๋์ค ๊ตฌํ ๋ฐ QueryDSL ์ฌ์ฉ ๋ฐฉ๋ฒ์ ๋ํด ๋ค๋ฃน๋๋ค.
๊ตฌํ ๋ฐฉ๋ฒ์๐งญ ํ๊ฒฝ ์ค์ ๋ถํฐ ๋์ ์์ต๋๋ค.
QueryDSL์ ์ ์ ํ์ ์ ์ด์ฉํด SQL๊ณผ ๊ฐ์ ์ฟผ๋ฆฌ๋ฅผ ์์ฑํ ์ ์๋๋ก ์ง์ํ๋ ์คํ์์ค ํ๋ ์์ํฌ์ด๋ค. ์ฟผ๋ฆฌ๋ฅผ ๋ฌธ์์ด๋ก ์์ฑํ๋ ๊ฒ์ด ์๋, QueryDSL์ด ์ ๊ณตํ๋ Fluent API๋ฅผ ์ด์ฉํด ์ฝ๋ ํ์์ผ๋ก ์ฟผ๋ฆฌ๋ฅผ ์์ฑํ๊ณ ์ ์ฉํ ์ ์๋๋ก ํ๋ค. ์ฝ๊ฒ ๋งํ๋ฉด ์ฝ๋๋ก ์ฟผ๋ฆฌ๋ฅผ ์กฐ๋ฆฝํ ์ ์๊ฒ ๋ง๋ค์ด์ ๋์ ์ฟผ๋ฆฌ๋ฅผ ์ง์ํ๋ค๋ ๋ง์ด๋ค.
ํ์ฌ ๊ฐ๋ฐ ์ค์ธ ํ๋ก์ ํธ์ ์๊ตฌ์ฌํญ ์ค์๋ ์ด๋ฏธ์ง์ ์ ๋ณด์ ๋ํ ๋ค์ค ์กฐ๊ฑด ๊ฒ์ ์ง์์ ๊ดํด ๋ช
์๋์ด์๋ค. ๋ค์ค ์กฐ๊ฑด ๊ฒ์์ ์ํด์๋ ๋์ ์ฟผ๋ฆฌ๋ฅผ ์์ฑํด์ผํ๋๋ฐ, ๋ค์ค ์กฐ๊ฑด ๊ฒ์ ๊ตฌํ ์ ์๋ JPQL์ ์ฌ์ฉํ์๋ค. ์ ์ฒด ๋ฆฌ์คํธ์์ ์ ํด์ง ์กฐ๊ฑด์ ๋ง์กฑํ๋ ๊ฒฐ๊ณผ๋ง ๊ฒ์ํ๋ฉด ๋๊ธฐ์ ์ ์ ์ฟผ๋ฆฌ์ธ JPQL๋ง ์ฌ์ฉํด๋ ๊ตฌํํ ์ ์์๊ธฐ ๋๋ฌธ์ด๋ค. ์ด์ ์ QueryDSL์ ์ฌ์ฉํด๋ณธ ์ ์ด ์์๊ธฐ์ ๊ดํ ์ด๋ ต๊ฒ ๋๊ปด์ ธ JPQL์์ ๋์ ์ฟผ๋ฆฌ๋ฅผ ์ฌ์ฉํ ์ ์๋ ๋ฐฉ๋ฒ์ด ์์๊น ๊ณ ๋ฏผํ๊ณ ์ฐพ์๋ดค๋ค. JPQL์์ ๋์ ์ฟผ๋ฆฌ๋ฅผ ๊ตฌํํ๊ธฐ ์ํด์๋ ์กฐ๊ฑด์ ๋ฐ๋ผ ์ฟผ๋ฆฌ๋ฅผ ์๋์ผ๋ก String์ผ๋ก ๋ง๋ค์ด ํ๋ผ๋ฏธํฐ๋ก ๋ฃ์ด์ฃผ๋ ๊ท์ฐฎ์ ๊ณผ์ ์ ๊ฑฐ์ณ์ผ ํ๋ค. ๊ฒฐ๊ตญ JPQL๋ก ๋์ ์ฟผ๋ฆฌ๋ฅผ ๊ตฌํํ๋ ๊ฒ์ด ํ๋ ์ฝ๋ฉ๊ณผ ๋ค๋ฅผ ๋ฐ๊ฐ ์๋ค๊ณ ํ๋จํด ๋์ ์ฟผ๋ฆฌ๋ฅผ ์ง์ํ๋ QueryDSL์ ์ฌ์ฉํ๊ธฐ๋ก ๊ฒฐ์ ํ๋ค.
ํด๋น ํ๋ก์ ํธ์์๋ ๋ฌดํ ์คํฌ๋กค ๊ตฌํ์ ์ํด pagination์ ์ง์ํ๋ค. ์ด๋ ํ์ด์ง์ ์ํด ํ๋ก ํธ๋ก๋ถํฐ ๋ฐ์์ค๋ ์กฐ๊ฑด๋ค(page, size, sort, keyword) ์ค ์ ๋ ฌ ์กฐ๊ฑด์ด ์ ์ฉ๋์ง ์๋ ๊ฒ์ ํ์ธํ๋ค. ์ปค์คํ
ํ์ง ์์ QueryDSL์ ์ฌ์ฉํ์ ๋, pagination ์์ฒด๋ ์ ์ฉ์ด ๋์์ง๋ง ์ ๋ ฌ ์กฐ๊ฑด์ ์ ์ฉ๋์ง ์์๋ค. ์ฐพ์๋ณด๋ ๋์ ์ ๋ ฌ์ ์ํด OrderSpecifier๋ฅผ ์ง์ ์ค์ ํด์ค์ผ ํ๋ค. ๋์ ์ ๋ ฌ ์ ์ฉ๊ณผ QueryDSL์ ๋ ํธํ๊ฒ ์ฌ์ฉํ๊ธฐ ์ํด Querydsl4RepositorySupport๋ฅผ ์ปค์คํ
ํด ์ฌ์ฉํ๋ค.
QueryDSL์ ์ฌ์ฉํ๊ธฐ ์ํ ์์กด์ฑ๊ณผ source set์ ์ค์ ํด์ค๋ค. source set์์๋ java compile ํ ์๊ธฐ๋ Q domain class๋ค์ ์์ฑ ์์น๋ฅผ ์ค์ ํด์ค ์ ์๋ค.
์๋ ์ค์ ์์๋ QueryDSL 5.1.0์ ์ฌ์ฉํ๊ณ ์๋ค. QueryDSL์ 4.x.x๊ฐ ๊ฐ์ฅ ๋ง์ด ์ฌ์ฉ๋์ง๋ง, 5.x.x ๋ฒ์ ์ด release ๋๋ฉด์ fetchResults()์ fetchCount()๊ฐ deprecated ๋์๊ธฐ์ 5.0.0 ์ดํ ๋ฒ์ ์ ์ฌ์ฉํ๋ ๊ฒ ํฅํ ์ ์ง๋ณด์์ฑ์ ์ข์ ๊ฒ์ด๋ผ ํ๋จํด QueryDSL 5.1.0๋ฅผ ์ฌ์ฉํ๋ค.
...
dependencies {
...
// querydsl ์์กด์ฑ ์ถ๊ฐ
implementation 'com.querydsl:querydsl-jpa:5.1.0:jakarta'
annotationProcessor "com.querydsl:querydsl-apt:${dependencyManagement.importedProperties['querydsl.version']}:jakarta"
implementation 'com.querydsl:querydsl-core'
annotationProcessor 'jakarta.persistence:jakarta.persistence-api'
annotationProcessor 'jakarta.annotation:jakarta.annotation-api'
...
}
...
// source set ์ค์
sourceSets {
main {
java {
// Q domain class๋ค์ด src/main/generated์ ์์ฑ๋๋๋ก ์ค์
srcDirs = ['src/main/java', 'src/main/generated']
}
}
}
...
์์กด์ฑ ์ค์ ์ด ์ ๋๋ก ๋์๋ ์ง ํ์ธํ๊ธฐ ์ํด Q domain class๋ฅผ ์์ฑํด ํ
์คํธํ๋ค. Q domain class๋ ๊ธฐ์กด์ ๋ง๋ค์ด์ ธ์๋ JPA repository๋ค์ domain entity class๋ฅผ ๋ฐํ์ผ๋ก ์์ฑ๋๋ค. ๊ทธ๋ฌ๋๊น Q domain class๋ฅผ ๋ง๋ค๊ธฐ ์ํด์๋ ๋น์ฐํ domain entity class๋ฅผ ๋ง๋ค์ด๋์ผ ํ๋ค.
Q domain class๋ฅผ ์์ฑํ๊ธฐ ์ํด์๋ ํ๋ก์ ํธ ๋ด์ Gradle ๋ฉ๋ด์์ build/clean ์คํ โ other/compileJava ๋๋ build/build๋ฅผ ์คํํ๋ฉด ๋๋ค. ์คํ ํ ์์ ์ค์ ํด๋ sourceSet ๋ด๋ถ์ Q domain class๋ค์ด ์์ฑ๋๋ฉด ์ ๋๋ก ์ค์ ๋ ๊ฒ์ด๋ค.

์๋์ ๊ธ์ ์ด๋ฏธ JPA Repository๋ฅผ ์ด์ฉํด ๊ตฌํ๋ domain entity์ repository class๊ฐ ์๋ค๋ ๊ฐ์ ํ์ ์์ฑ๋์์ต๋๋ค.
์ฌ๊ธฐ์์๋ ๋ค์ค ์กฐ๊ฑด ๊ฒ์๋ง์ ์ํ SearchContentRepository ์ธํฐํ์ด์ค๋ฅผ ์ ์ธํด์ฃผ์๋ค.
package example.api.content.repository.search;
public interface SearchContentRepository {
Page<ContentViewDto> searchByKeyword(Pageable pageable, String keyword,
List<Long> uploadDateRange, AccountEntity account);
}
๊ธฐ์กด์ ์ฌ์ฉํ๋ JPA repository(์ฌ๊ธฐ์๋ ContentRepository)์ ์์ ์ ์ธํ SearchContentRepository ์ธํฐํ์ด์ค๋ฅผ ๋ถ๋ชจ๋ก ์ค์ ํด์ค๋ค.
package example.api.content.repository;
@Repository
public interface ContentRepository extends JpaRepository<ContentEntity, Long>, SearchContentRepository {
ContentEntity findByContentId(String contentId);
ContentEntity findByContentIdAndAccount(String contentId, AccountEntity account);
}
๋์ ์ ๋ ฌ ์ง์์ ์ํด pageable์ sort๋ก OrderSpecifier๋ฅผ ์์ฑํ๋ getOrderSpecifier() ๋ฉ์๋๋ฅผ ์ถ๊ฐํ๊ณ , pagination์ ์ง์ํ๋๋ก applyPagination() ๋ฉ์๋๋ฅผ ์ถ๊ฐํด ์ปค์คํฐ๋ง์ด์งํ๋ค.
package example.global.querydsl;
import com.querydsl.core.types.EntityPath;
import com.querydsl.core.types.Expression;
import com.querydsl.core.types.Order;
import com.querydsl.core.types.OrderSpecifier;
import com.querydsl.core.types.dsl.PathBuilder;
import com.querydsl.jpa.impl.JPAQuery;
import com.querydsl.jpa.impl.JPAQueryFactory;
import jakarta.persistence.EntityManager;
import java.util.ArrayList;
import java.util.List;
import java.util.function.Function;
import lombok.Getter;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.domain.Page;
import org.springframework.data.domain.Pageable;
import org.springframework.data.domain.Sort;
import org.springframework.data.jpa.repository.support.JpaEntityInformation;
import org.springframework.data.jpa.repository.support.JpaEntityInformationSupport;
import org.springframework.data.jpa.repository.support.Querydsl;
import org.springframework.data.querydsl.SimpleEntityPathResolver;
import org.springframework.data.support.PageableExecutionUtils;
import org.springframework.stereotype.Repository;
/**
* Querydsl4 repository support
*/
@Repository
@Getter
public abstract class Querydsl4RepositorySupport {
private final Class domainClass;
private Querydsl querydsl;
private EntityManager entityManager;
private JPAQueryFactory queryFactory;
public Querydsl4RepositorySupport(Class domainClass) {
this.domainClass = domainClass;
}
/**
* Set entity manager
*
* @param entityManager entity manager
*/
@Autowired
public void setEntityManager(EntityManager entityManager) {
JpaEntityInformation entityInformation = JpaEntityInformationSupport.getEntityInformation(
domainClass, entityManager);
SimpleEntityPathResolver resolver = SimpleEntityPathResolver.INSTANCE;
EntityPath path = resolver.createPath(entityInformation.getJavaType());
this.entityManager = entityManager;
this.querydsl = new Querydsl(entityManager,
new PathBuilder<>(path.getType(), path.getMetadata()));
this.queryFactory = new JPAQueryFactory(entityManager);
}
/**
* Select query - ์ฟผ๋ฆฌ ์์ฑ์ ์ํ ๋ฉ์๋
*
* @param expr ์ฟผ๋ฆฌ ๋์
* @param <T> ์ฟผ๋ฆฌ ๊ฒฐ๊ณผ ํ์
* @return JPAQuery
*/
protected <T> JPAQuery<T> select(Expression<T> expr) {
return getQueryFactory().select(expr);
}
/**
* Select query - ์ฟผ๋ฆฌ ์์ฑ์ ์ํ ๋ฉ์๋
*
* @param from ์ฟผ๋ฆฌ ๋์
* @param <T> ์ฟผ๋ฆฌ ๊ฒฐ๊ณผ ํ์
* @return JPAQuery
*/
protected <T> JPAQuery<T> selectFrom(EntityPath<T> from) {
return getQueryFactory().selectFrom(from);
}
/**
* Apply pagination - ํ์ด์ง ์ฒ๋ฆฌ๋ฅผ ์ํ ๋ฉ์๋
*
* @param pageable ํ์ด์ง ์ ๋ณด
* @param contentQuery ์ฝํ
์ธ ์ฟผ๋ฆฌ
* @param countQuery ์นด์ดํธ ์ฟผ๋ฆฌ
* @param <T> ํ์ด์ง ๊ฒฐ๊ณผ ํ์
* @return ํ์ด์ง ๊ฒฐ๊ณผ
*/
protected <T> Page<T> applyPagination(Pageable pageable,
Function<JPAQueryFactory, JPAQuery> contentQuery,
Function<JPAQueryFactory, JPAQuery> countQuery,
Class<?> sortTargetClass,
String sortVariableName) {
JPAQuery jpaContentQuery = contentQuery.apply(getQueryFactory());
List<OrderSpecifier> orders = getOrderSpecifier(pageable.getSort(), sortTargetClass,
sortVariableName);
if (!orders.isEmpty()) {
jpaContentQuery.orderBy(orders.toArray(new OrderSpecifier[0]));
}
List<T> content = getQuerydsl().applyPagination(pageable, jpaContentQuery).fetch();
// JPAQuery countResult = countQuery.apply(getQueryFactory());
Long totalCount = countQuery.apply(getQueryFactory()).fetchCount();
return PageableExecutionUtils.getPage(content, pageable, () -> totalCount);
}
/**
* Get order specifier - ๋์ ์ ๋ ฌ์ ์ํ ๋ฉ์๋
*
* @param sort ์ ๋ ฌ ์กฐ๊ฑด
* @param sortTargetClass ์ ๋ ฌ ๋์ ํด๋์ค
* @param sortVariableName ์ ๋ ฌ ๋์ ๋ณ์๋ช
* @return ์ ๋ ฌ ์กฐ๊ฑด
*/
private List<OrderSpecifier> getOrderSpecifier(Sort sort, Class<?> sortTargetClass,
String sortVariableName) {
List<OrderSpecifier> orders = new ArrayList<>();
// Sort
sort.stream().forEach(order -> {
Order direction = order.isAscending() ? Order.ASC : Order.DESC;
String prop = order.getProperty();
PathBuilder orderByExpression = new PathBuilder(sortTargetClass, sortVariableName);
orders.add(new OrderSpecifier(direction, orderByExpression.get(prop)));
});
return orders;
}
}
์์ ๋ง๋ SearchContentRepository ์ธํฐํ์ด์ค๋ฅผ ๊ตฌํํ SearchStudyRepositoryImpl ํด๋์ค๋ฅผ ๊ตฌํํ๋ค. ์ด๋ Querydsl4RepositorySupport ์ถ์ ํด๋์ค๋ฅผ ์์ํด ์์์ ๊ตฌํํ ๋ฉ์๋๋ฅผ ์ฌ์ฉํ ์ ์๋๋ก ํ๋ค.
์กฐ๊ฑด ์ถ๊ฐ ์ ์กฐ๊ฑด์ด null์ธ์ง ํ์ธ ํ builder์ ์ถ๊ฐํด์ฃผ๋ ๋ฐฉ์์ผ๋ก ๊ตฌํํ ์ ์๋ค.
package example.api.content.repository.search;
import com.querydsl.core.BooleanBuilder;
import com.querydsl.core.types.Projections;
import com.querydsl.jpa.JPQLQuery;
import com.querydsl.jpa.impl.JPAQuery;
import com.querydsl.jpa.impl.JPAQueryFactory;
import java.util.List;
import example.api.account.domain.AccountEntity;
import kr.co.beamworks.cadaimbe.api.account.domain.QAccountEntity;
import kr.co.beamworks.cadaimbe.api.patient.domain.QPatientEntity;
import kr.co.beamworks.cadaimbe.api.study.domain.QStudyEntity;
import kr.co.beamworks.cadaimbe.api.study.domain.StudyEntity;
import kr.co.beamworks.cadaimbe.api.study.dto.response.StudyViewDto;
import kr.co.beamworks.cadaimbe.global.querydsl.Querydsl4RepositorySupport;
import kr.co.beamworks.cadaimbe.global.util.time.TimeConverter;
import org.springframework.data.domain.Page;
import org.springframework.data.domain.Pageable;
import org.springframework.data.jpa.repository.support.QuerydslRepositorySupport;
import org.springframework.stereotype.Repository;
@Repository
public class SearchStudyRepositoryImpl extends Querydsl4RepositorySupport implements SearchStudyRepository {
/**
* Creates a new {@link QuerydslRepositorySupport} instance for the given domain type.
*/
public SearchStudyRepositoryImpl() {
super(StudyEntity.class);
}
/**
* Search study by keyword
*
* @param pageable pageable
* @param keyword keyword
* @param studyDateRange study date range(Unix time)
* @param uploadDateRange upload date range(Unix time)
* @param cadaiScoreRange cadai score range(0.0 ~ 100.0)
* @param account account entity
* @return study view dto page
*/
@Override
public Page<StudyViewDto> searchByKeyword(Pageable pageable, String keyword,
List<Long> uploadDateRange, List<Double> cadaiScoreRange, AccountEntity account) {
JPAQueryFactory queryFactory = new JPAQueryFactory(getEntityManager());
QContentEntity studyEntity = QContentEntity.contentEntity;
QAccountEntity accountEntity = QAccountEntity.accountEntity;
BooleanBuilder builder = new BooleanBuilder();
if (keyword != null && !keyword.isEmpty()) {
builder.or(contentEntity.contentId.like("%" + keyword + "%"));
builder.or(contentEntity.title.like("%" + keyword + "%"));
builder.or(accountEntity.institution.like("%" + keyword + "%"));
builder.or(contentEntity.description.like("%" + keyword + "%"));
}
// Add account condition if account is not null
if (account != null) {
builder.and(accountEntity.eq(account));
}
// Add range conditions
if (uploadDateRange != null && uploadDateRange.size() == 2) {
builder.and(contentEntity.uploadDate.between(TimeConverter.convertUnixTimeToLocalDateTime(uploadDateRange.get(0)),
TimeConverter.convertUnixTimeToLocalDateTime(uploadDateRange.get(1))));
}
// ๋์ ์ฟผ๋ฆฌ ์์ฑ
JPQLQuery<ContentViewDto> query = queryFactory
.select(Projections.constructor(
ContentViewDto.class,
contentEntity.contentId,
contentEntity.title,
accountEntity.institution,
contentEntity.description,
contentEntity.uploadDate))
.from(contentEntity)
.join(contentEntity.account, accountEntity)
.where(builder);
// pagination ์ ์ฉ
return applyPagination(pageable,
factory -> (JPAQuery) query,
factory -> (JPAQuery) query,
ContentEntity.class,
"contentEntity"
);
}
}
ํ๋ก์ ํธ๋ฅผ ๋น ๋ฅด๊ฒ ๋๋ด๋ ๊ฒ ๊ฐ์ฅ ํฐ ๋ชฉํ์๊ธฐ ๋๋ฌธ์ JPQL๊ณผ QueryDSL์ ํจ๊ป ์ฌ์ฉํ๋ ๋ฐฉ์์ผ๋ก ๊ตฌํ๋๊ณ ์๋ค. ํ์ง๋ง QueryDSL์ ์ฌ์ฉํ๋ ์ด์ ๊ตณ์ด JPQL์ ์ฌ์ฉํ ์ด์ ๊ฐ ์๊ธฐ์ QueryDSL๋ง์ ์ฌ์ฉํด ๊ฒ์ ๊ธฐ๋ฅ์ ๊ตฌํํ ์ ์๋๋ก ๋ฆฌํฉํ ๋งํ ์์ ์ด๋ค.
์๋ฅผ ๋ค์ด ์ด๋ฆ์ ๊ฒ์ํ ๋ ์ ๋ชฉ์ด 'Hello world'์ธ ๊ฒ์๊ธ์ด ์๋ค๊ณ ํ์. ํ์ฌ๊น์ง ๊ตฌํ๋ ๊ฒ์ ๊ธฐ๋ฅ์์๋ Hello w, Hel ๋ฑ์ ๊ฒ์ ์ ํด๋น ๊ฒ์๊ธ์ด ๋์ค์ง๋ง hello world๋ฅผ ๊ฒ์ํ์ ๋๋ ๋์ค์ง ์๋๋ค. ์ด์ฒ๋ผ lower case, ๋์ด์ฐ๊ธฐ๊ฐ ์๋ต๋ ๊ฒ์์ด์ ๋ํด ๋ ์ ์ฐํ๊ฒ ๊ฒ์์ ์ง์ํ ์ ์๋๋ก ์ถ๊ฐ์ ์ธ ๋ฆฌ์์น์ ๊ฐ๋ฐ์ด ํ์ํ๋ค.
Reference : querydsl์ ์ด์ฉํ ํ์ด์ง/๊ฒ์ ๊ธฐ๋ฅ ๊ตฌํ, [Querydsl]๋์ sorting์ ์ํ OrderSpecifier ํด๋์ค ๊ตฌํ, QueryDSL ๊ณต์ ํํ์ด์ง