public interface OrderDataDao {
Optional<OrderData> findById(OrderNo id);
List<OrderData> findByOrderer(String orderId, Date formDate, Date toDate);
...
}
public interface Speficiation<T> {
public boolean isSatisfiedBy(T agg);
}
스펙 구현 클래스
public class OrdererIdSpec implements Specification<OrderSummary> {
private String ordererId;
@Override
public Predicate toPredicate(
Root<OrderSummary> root,
CriteriaQuery<?> query,
CriteriaBuilder cb
) {
return cb.equal(root.get(OrderSummary_.ordererId), ordererId);
}
}
@StaticMetamodel(OrderSummary.class)
public class OrderSummary_ {
public static volatile SingularAttribute<OrderSummary, String> number;
public static volatile SingularAttribute<OrderSummary, Long> number;
public static volatile SingularAttribute<OrderSummary, String> ordererId;
public static volatile SingularAttribute<OrderSummary, String> ordererName;
...
}
스펙 생성 기능을 별도 클래스에 모은 예
public class OrderSummarySpecs {
public static Specification<OrderSummary> ordererId(String ordererId) {
return (Root<OrderSummary> root, CriteriaQuey<?> query,
CriteriaBuilder cb) ->
cb.equal(root.get(OrderSummary_.orderId), orderId);
}
public static Specification<OrderSummary> orderDateBetween(LocalDateTime from, LocalDateTime to) {
return (Root<OrderSummary> root, CriteriaQuey<?> query,
CriteriaBuilder cb) ->
cb.equal(root.get(OrderSummary_.orderDate), from, to);
}
}
→ 같은 애그리거트라면 스펙 생성 기능을 별도 클래스에 한번에 모아두는 것이 파일 수도 줄이고 개발자가 관리하기 쉬울 것 같다는 생각이 든다.
public interface OrderSummaryDao extends Repository<OrderSummary, String> {
List<OrderSummary> findAll(Specification<OrderSummary> spec);
}
List<OrderSummary> findByOrdererIdOrderByNumberDesc(String ordererId);
// ordererId 프로퍼티 값을 기준으로 검색 조건 지정
// number 프로퍼티 값 역순으로 정렬
-> 메서드의 길이가 길어지면서 가독성 감소, 메서드 이름으로 정렬 순서 정해지기 때문에 상황에 따른 정렬 순서 변경 어려움
Sort sort = Sort.by("number").ascending();
List<OrderSummary> results = orderSummaryDao.findByOrdererId("user1", sort);
List<MemberData> findByNameLike(String name, Pageable pagealbe);
PageRequest pageReq = PageRequest.of(1, 10);
List<MemberData> user = memberDataDao.findByNameLike("사용자%", pageReq);
public class SpecBuilder {
public static <T> Builder<T> builder(Class<T> type) {
return new Builder<T>();
}
public static class Builder<T> {
private List<Specification<T>> specs = new ArrayList<>();
public Builder<T> and(Specification<T> spec) {
specs.add(spec);
return this;
}
public Builder<T> ifHasText(String str,
Function<String, Specification<T>> specSupplier) {
if (StringUtils.hasText(str)) {
specs.add(specSupplier.apply(str));
}
return this;
}
public Builder<T> ifTrue(Boolean cond,
Supplier<Specification<T>> specSupplier) {
if (cond != null && cond.booleanValue()) {
specs.add(specSupplier.get());
}
return this;
}
public Specification<T> toSpec() {
Specification<T> spec = Specification.where(null);
for (Specification<T> s : specs) {
spec = spec.and(s);
}
return spec;
}
}
}
// 조건문으로 인해 복잡해 보임, 실수하기도 좋음
@Test
void compositeSpec() {
SearchRequest searchRequest = new SearchRequest();
Specification<MemberData> spec = Specification.where(null);
if (searchRequest.isOnlyNotBlocked()) {
spec = spec.and(MemberDataSpecs.nonBlocked());
}
if (StringUtils.hasText(searchRequest.getName())) {
spec = spec.and(MemberDataSpecs.nameLike(searchRequest.getName()));
}
List<MemberData> result = memberDataDao.findAll(spec, PageRequest.of(0, 5));
logger.info("result: {}", result.size());
}
// 가독성 향상
@Test
void specBuilder() {
SearchRequest searchRequest = new SearchRequest();
searchRequest.setOnlyNotBlocked(true);
Specification<MemberData> spec = SpecBuilder.builder(MemberData.class)
.ifTrue(
searchRequest.isOnlyNotBlocked(),
() -> MemberDataSpecs.nonBlocked())
.ifHasText(
searchRequest.getName(),
name -> MemberDataSpecs.nameLike(searchRequest.getName()))
.toSpec();
List<MemberData> result = memberDataDao.findAll(spec, PageRequest.of(0, 5));
logger.info("result: {}", result.size());
}
@Query("""
select new com.myshop.order.query.dto.OrderView(
o.number, o.state, m.name, m.id, p.name
)
from Order o join o.orderLines ol, Member m, Product p
where o.orderer.memberId.id = :ordererId
and o.orderer.memberId.id = m.id
and index(ol) = 0
and ol.productId.id = p.id
order by o.number.number desc
""")
List<OrderView> findOrderView(String ordererId);
→ 이 방식 대신 Querydsl을 사용하는 것이 더 가독성 있고 동적 객체를 생성하기 좋아보이기도 함
@Entity
@Immutable
@Subselect(
"""
select o.order_number as number,
o.version,
o.orderer_id,
o.orderer_name,
o.total_amounts,
o.receiver_name,
o.state,
o.order_date,
p.product_id,
p.name as product_name
from purchase_order o inner join order_line ol
on o.order_number = ol.order_number
cross join product p
where
ol.line_idx = 0
and ol.product_id = p.product_id"""
)
@Synchronize({"purchase_order", "order_line", "product"})
public class OrderSummary {
...
}