Front-End에서 데이터 테이블을 이용하여 데이터를 보여주고 있었습니다. 이 때 각 컬럼의 Sort를 설정하여 데이터를 정렬하는 Querydsl을 사용하기 위해 적용한 내용을 정리한 글입니다.
모든 소스는 Github에 있습니다.
사용하게 된 이유
화면에서 넘겨주는 Sort관련 정보는 N개의 테이블에 대한 정보가 넘어올 수 있습니다.
예를 들어 Store, Staff 2개의 테이블이 존재하고 같은 field의 이름이 존재합니다.
@Entity
@Getter
@NoArgsConstructor(access = AccessLevel.PROTECTED)
public class Store {
@Id
@GeneratedValue(strategy = GenerationType.AUTO)
private Long id;
private String name; // 동일
private String address;
@OneToMany(mappedBy = "store", cascade = CascadeType.ALL, orphanRemoval = true, fetch = FetchType.LAZY)
private List<Staff> staffs = new ArrayList<>();
@Builder
public Store(Long id, String name, String address, List<Staff> staffs) {
this.id = id;
this.name = name;
this.address = address;
this.staffs = staffs;
}
public void addStaff(Staff staff) {
if (this.staffs == null) {
this.staffs = new ArrayList<>();
}
staffs.add(staff);
staff.setStore(this);
}
}
@Entity
@Getter
@NoArgsConstructor(access = AccessLevel.PROTECTED)
public class Staff {
@Id
@GeneratedValue(strategy = GenerationType.AUTO)
private Long id;
private String name; // 동일
private Integer age;
private String lastName;
@ManyToOne(fetch = FetchType.EAGER, cascade = CascadeType.ALL)
@JoinColumn(name = "store_id", foreignKey = @ForeignKey(name = "fk_staff_store_id"))
private Store store;
@Builder
public Staff(Long id, String name, Integer age, String lastName, Store store) {
this.id = id;
this.name = name;
this.age = age;
this.lastName = lastName;
this.store = store;
}
public void changeName(final String name) {
this.name = name;
}
public void setStore(Store store) {
this.store = store;
}
}
Store, Staff Entity 모두 name이라는 field를 가지고 있습니다.
이런 경우 store의 name과 staff의 name으로 정렬하려면 어떻게 해야할까?
queryFactory
.selectFrom(store)
.join(store.staffs, staff)
Pageable로 테스트
위와 같은 쿼리라고 했을 때 Sort 설정을 할 때 join의 정보를 명시하면 됩니다.
@ActiveProfiles("h2")
@ExtendWith(SpringExtension.class)
@SpringBootTest
@TestConstructor(autowireMode = TestConstructor.AutowireMode.ALL)
class StaffRepositoryTest {
private final StaffRepository staffRepository;
private final StoreRepository storeRepository;
public StaffRepositoryTest(StaffRepository staffRepository, StoreRepository storeRepository) {
this.staffRepository = staffRepository;
this.storeRepository = storeRepository;
}
@Test
@DisplayName("querydsl join 시 paging 테스트 - pageable 사용 경우 ")
void findStaffJoinOrderPaging_test_if_use_pageable() {
// when
Sort.Order storeNameOrder = Sort.Order.asc("name"); // store
Sort.Order staffNameOrder = Sort.Order.asc("staffs.name"); // staff
Sort sort = Sort.by(storeNameOrder, staffNameOrder);
Pageable pageable = PageRequest.of(0, 10, sort);
// then
PageImpl<Store> pageImplStaff = staffRepository.findStoreJoinOrderPaging(pageable); // test
}
}
테이블을 명시하지 않고 필드명만 입력하는 경우 from 절에 있는 table의 필드로 적용됩니다.
Order 객체를 생성할 때 파라미터로 필드명을 명시하고 Pageable을 생성하여 테스트합니다.
public PageImpl<Store> findStoreJoinOrderPaging(Pageable pageable) {
JPAQuery<Store> staffQuery = queryFactory
.selectFrom(store)
.join(store.staffs, staff);
return pagingUtil.getPageImpl(pageable, staffQuery, Store.class);
}
@Component
@RequiredArgsConstructor
public class PagingUtil {
private final EntityManager entityManager;
private Querydsl getQuerydsl(Class clazz) {
PathBuilder<Staff> builder = new PathBuilderFactory().create(clazz);
return new Querydsl(entityManager, builder);
}
public <T> PageImpl<T> getPageImpl(Pageable pageable, JPQLQuery<T> query, Class clazz) {
long totalCount = query.fetchCount();
List<T> results = getQuerydsl(clazz).applyPagination(pageable, query).fetch(); // pageable 적용하여 query 실행
return new PageImpl<>(results, pageable, totalCount);
}
}
생성된 쿼리에 Pageable을 적용시켜 쿼리를 실행시킵니다.
select
store0_.id as id1_3_,
store0_.address as address2_3_,
store0_.name as name3_3_
from
store store0_
inner join
staff staffs1_
on store0_.id=staffs1_.store_id
order by
store0_.name asc,
staffs1_.name asc limit 10
예상했던 그림의 query가 나오는 것을 알 수 있습니다.
여기서 부터는 위 기능이 지원되지 않는 줄 알고 기능을 만들었습니다.... 자세히 알아볼껄.. 그래서 기능을 만들었으니 블로그에는 남겨놓자.. ㅠ
원래 이런 기능이 지원되지 않는 줄 알고 다른 방법으로 구현을 하고 정리하려했던 글입니다... ㅠ
블로그 작성을 위해 다시 자세히 알아보던 중 되네? 이것도 되네? 다 되네?? 하다가 글의 방향을 틀었습니다.
위 기능을 Sort가 아닌 제가 만든 객체로 사용하여 동일하게 구현하겠습니다.
Pageable 과 비슷한 Vo
@ToString
@Getter @Setter
public class PageOrderVo {
private Integer page;
private Integer pageSize;
private List<SortVo> sorts;
}
Sort와 비슷한 Vo
@Getter @Setter
public class SortVo {
private String key;
private OrderSort orderSort;
public boolean equalsAsc() {
return OrderSort.ASC.equals(this.orderSort);
}
public boolean equalsDesc() {
return OrderSort.DESC.equals(this.orderSort);
}
}
asc, desc 열거형
@Getter
public enum OrderSort {
ASC("asc"),
DESC("desc");
private String name;
OrderSort(String name) {
this.name = name;
}
}
테스트 코드
@Test
@DisplayName("querydsl join 시 paging 테스트 - (성공)")
void findStaffJoinOrderPaging_test() {
//given
PageOrderVo pageOrderVo = new PageOrderVo();
SortVo storeNameSort = new SortVo(); // store
storeNameSort.setKey("name"); // store name
storeNameSort.setOrderSort(OrderSort.DESC);
SortVo staffNameSort = new SortVo(); // staff
staffNameSort.setKey("staffs.name"); // staff name
staffNameSort.setOrderSort(OrderSort.DESC);
pageOrderVo.setPage(0);
pageOrderVo.setPageSize(10);
pageOrderVo.setSorts(Arrays.asList(storeNameSort, staffNameSort));
//when
PageImpl<Store> pageImplStaff = staffRepository.findAllJoinDynamicOrder(pageOrderVo);
//then
assertAll(
() -> assertThat(pageImplStaff.getTotalPages()).isEqualTo(1),
() -> assertThat(pageImplStaff.getNumber()).isEqualTo(0),
() -> assertThat(pageImplStaff.getContent()).hasSize(10)
);
}
Querydsl로 생성된 Query에 Page, Order 적용하여 실행하는 메서드
@Override
public PageImpl<Store> findAllJoinDynamicOrder(PageOrderVo pageOrderVo) {
JPAQuery<Store> query = queryFactory
.selectFrom(store)
.join(store.staffs, staff);
OrderPage orderPage = new StoreOrderPage(entityManager, Store.class);
return orderPage.getPageImpl(query, pageOrderVo);
}
Query에 Page와 Order를 적용하는 추상화 클래스
StoreOrderPage라는 클래스를 만들었습니다. 이 클래스는 OrderPage 클래스를 상속받는 클래스입니다. 먼저 OrderPage 클래스는 추상화 클래스입니다.
public abstract class OrderPage {
protected final List<OrderSpecifier<?>> orderSpecifiers; // order 정보를 담고 있는 리스트
private final EntityManager entityManager;
private final Class<?> returnTypeClass;
// constructor
protected OrderPage(EntityManager entityManager, Class<?> returnTypeClass) {
this.orderSpecifiers = new ArrayList<>();
this.entityManager = entityManager;
this.returnTypeClass = returnTypeClass;
}
// pageImp 객체를 리턴합니다.
public <T> PageImpl<T> getPageImpl(JPQLQuery<T> query, PageOrderVo pageOrderVo) {
long totalCount = query.fetchCount();
JPQLQuery<T> applyOrderQuery = applyOrderQuery(query, pageOrderVo.getSorts());
return getPageImplByPage(applyOrderQuery, pageOrderVo, totalCount);
}
// 쿼리에 Order를 적용합니다.
private <T> JPQLQuery<T> applyOrderQuery(JPQLQuery<T> query, List<SortVo> sorts) {
if (sorts == null || sorts.isEmpty()) {
return query;
}
return query.orderBy(getOrder(sorts));
}
// Pageable 객체를 생성하고 리턴합니다.
private Optional<Pageable> getPageable(Integer page, Integer pageSize) {
if (page == null || pageSize == null) {
return Optional.empty();
}
return Optional.of(PageRequest.of(page, pageSize));
}
// Pageable을 적용한 pageImpl 객체를 리턴합니다.
private <T> PageImpl<T> getPageImplByPage(JPQLQuery<T> query, PageOrderVo pageOrderVo, long totalCount) {
Optional<Pageable> optionalPageable = getPageable(pageOrderVo.getPage(), pageOrderVo.getPageSize());
if (optionalPageable.isPresent()) {
List<T> queryResults = getQuerydsl(returnTypeClass).applyPagination(optionalPageable.get(), query).fetch();
return new PageImpl<>(queryResults, optionalPageable.get(), totalCount);
}
return new PageImpl<>(query.fetch());
}
// Pageable을 적용하기 위한 Querydsl 객체를 리턴합니다.
private Querydsl getQuerydsl(Class<?> clazz) {
PathBuilder<?> builder = new PathBuilderFactory().create(clazz);
return new Querydsl(entityManager, builder);
}
/ **
* 자식 클래스에서 사용하는 메서드
* 내림차순, 오름차순 Order를 적용
* /
protected void addOrderSpecifier(SortVo sortVo, ComparableExpressionBase<?> comparableExpressionBase) {
if (sortVo.equalsAsc()) {
this.orderSpecifiers.add(comparableExpressionBase.asc());
} else if (sortVo.equalsDesc()) {
this.orderSpecifiers.add(comparableExpressionBase.desc());
}
}
// 추상클래스
protected abstract OrderSpecifier[] getOrder(List<SortVo> sorts);
}
메서드마다 Order 적용하는 클래스
조회하는 쿼리마다 생성해야하는 클래스입니다. OrderPage를 상속받고 getOrder 메서드를 구현합니다.
public class StoreOrderPage extends OrderPage {
public StoreOrderPage(EntityManager entityManager, Class returnTypeClass) {
super(entityManager, returnTypeClass);
}
@Override
public OrderSpecifier<?>[] getOrder(List<SortVo> sorts) {
for (SortVo sort : sorts) {
StoreOrder.getComparableExpressionBaseByName(sort.getKey()).ifPresent(
comparableExpressionBase -> super.addOrderSpecifier(sort, comparableExpressionBase)
);
}
return this.orderSpecifiers.toArray(new OrderSpecifier[0]);
}
}
파라미터로 넘어오는 SortVo 리스트에서 StoreOrder Enum에서 찾아서 생성된 쿼리에 orderBy절을 생성하여 붙혀줍니다.
QuerydslOrder는 Order 관련 Enum을 그룹화하기 위한 인터페이스입니다.
Order 할 수 있는 필드 열거형
public enum StoreOrder implements QuerydslOrder {
STORE_NAME("name", store.name),
STAFF_NAME("staffs.name", staff.name);
private final String name;
private final ComparableExpressionBase<?> comparableExpressionBase;
StoreOrder(String name, ComparableExpressionBase<?> comparableExpressionBase) {
this.name = name;
this.comparableExpressionBase = comparableExpressionBase;
}
@Override
public String getName() {
return this.name;
}
@Override
public ComparableExpressionBase<?> getComparableExpressionBase() {
return this.comparableExpressionBase;
}
public static Optional<ComparableExpressionBase<?>> getComparableExpressionBaseByName(String name) {
for (StoreOrder value : StoreOrder.values()) {
if (value.name.equals(name)) {
return Optional.of(value.comparableExpressionBase);
}
}
return Optional.empty();
}
}
public interface QuerydslOrder {
String getName();
ComparableExpressionBase<?> getComparableExpressionBase();
}
결과
다시 돌아와서 테스트하는 메서드를 실행합니다.
@Override
public PageImpl<Store> findAllJoinDynamicOrder(PageOrderVo pageOrderVo) {
JPAQuery<Store> query = queryFactory
.selectFrom(store)
.join(store.staffs, staff);
OrderPage orderPage = new StoreOrderPage(entityManager, Store.class);
return orderPage.getPageImpl(query, pageOrderVo);
}
select
store0_.id as id1_3_,
store0_.address as address2_3_,
store0_.name as name3_3_
from
store store0_
inner join
staff staffs1_
on store0_.id=staffs1_.store_id
order by
store0_.name desc,
staffs1_.name desc limit 10
결과는 Querydsl에서 제공하는 Pageable과 동일합니다.
결론
구현되어있는 것을 사용하자! Pageable을 자세히 파고 했다면 위와같은 엄청난 코드를 추가할 필요가 없었습니다. ㅠㅠ 반성..
'Develop > spring-data' 카테고리의 다른 글
[kotlin] Querydsl-JPA GroupBy 사용했을 경우 Paging처리 방법 (1) | 2021.08.06 |
---|---|
[Querydsl-JPA] 자주 사용하는 기능 정리 (Kotlin) (3) | 2021.05.10 |
[Querydsl] 성능개선 - 3편 ( group by, 커버링 인덱스, update ) (2) | 2021.02.04 |
[Querydsl] 성능개선 - 2편 ( N + 1 ) (0) | 2021.02.01 |
[Querydsl] 성능 개선 1편 (0) | 2021.01.29 |