public interface OrderRepository {
Order findById(OrderNo id);
List<Order> findByOrderer(String ordererId, Date fromDate, Date toDate);
...
}
public interface Specification<T> {
// agg 파라미터는 검사 대상이 되는 애그리거트 객체.
// 검사 대상 객체가 조건을 충족하면 true / 아니면 false를 리턴한다.
boolean isSatisfiedBy(T agg);
}
public List<Order> findAll(Specification spec) {
List<Order> allOrders = findAll();
return allOrders.stream()
.filter(order -> spec.isSatisfiedBy(order))
.collect(Collectors.toList());
}
Specification<Order> ordererSpec = new OrdererSpec("ordererId");
List<Order> orders = orderRepository.findAll(ordererSpec);
두 스펙을 And 연산자나 Or 연산자로 조합해서 새로운 스펙을 만들 수 있고
조합한 스펙을 다시 조합해 더 복잡한 스펙을 구현할 수 있다.
// AndSpec을 이용해 하나의 spec으로 생성할 수 있음.
public class AndSpec<T> implements Specification<T> {
private List<Specification<T>> specs;
public AndSpec(Specification<T>... specs) {
this.specs = Arrays.asList(specs);
}
@Override
public boolean isSatisfiedBy(T agg) {
for (Specification<T> spec : specs) {
if (!spec.isSatisfiedBy(agg)) {
return false;
}
}
return true;
}
...
}
// 여러 스펙을 하나의 스펙으로 만든 뒤 리포지터리에 전달할 수 있다.
Specification<Order> ordererSpec = new OrdererSpec("ordererId");
Specification<Order> orderDateSpec = new OrderDateSpec(fromDate, toDate);
AndSpec<T> combinedSpec = new AndSpec(ordererSpec, orderDateSpec);
List<Order> orders = orderRepository.findAll(combinedSpec);
import javax.persistence.criteria.CriteriaBuilder;
import javax.persistence.criteria.Predicate;
import javax.persistence.criteria.Root;
public interface Specification<T> {
Predicate toPredicate(Root<T> root, CriteriaBuilder cb);
}
public class OrdererSpec implements Specification<Order> {
private String ordererId;
public OrdererSpec(String ordererId) {
this.ordererId = ordererId;
}
@Override
public Predicate toPredicate(Root<Order> root, CriteriaBuilder cb) {
return cb.equal(root.get(Order_.orderer)
.get(Orderer_.memberId).get(MemberId_.id),
ordererId);
}
}
Orderer_.memberId.id
프로퍼티가 생성자로 전달받은 ordererId 와 같은지 비교하는 Predicate 를 생성해서 리턴한다.OrdererSpec spec = new OrdererSpec("ordererId");
List<Order> orders = orderRepository.findAll(spec);
import javax.persistence.metamodel.ListAttribute;
import javax.persistence.metamodel.SingularAttribute;
import javax.persistence.metamodel.StaticMetamodel;
@StaticMetamodel(Order.class)
public abstract class Order_ {
public static volatile SingularAttribute<Order, OrderNo> number;
public static volatile ListAttribute<Order, OrderLine> orderLines;
public static volatile SingularAttribute<Order, Orderer> orderer;
public static volatile SingularAttribute<Order, ShippingInfo> shippingInfo;
public static volatile SingularAttribute<Order, OrderState> state;
}
_
을 붙인 이름을 갖는다.// And 스펙 구현
public class AndSpecification<T> implements Specification<T> {
private List<Specification<T>> specs;
public AndSpecification(Specification<T> ... specs) {
this.specs = Arrays.asList(specs);
}
@Override
public Predicate toPredicate(Root<T> root, CriteriaBuilder cb) {
Predicate[] predicates = specs.stream()
.map(spec -> spec.toPredicate(root, cb))
.toArray(Predicate[]::new);
return cb.and(predicates);
}
}
// Or 스펙 구현
public class OrSpecification<T> implements Specification<T> {
private List<Specification<T>> specs;
public OrSpecification(Specification<T>... specs) {
this.specs = Arrays.asList(specs);
}
@Override
public Predicate toPredicate(Root<T> root, CriteriaBuilder cb) {
Predicate[] predicates = specs.stream()
.map(spec -> spec.toPredicate(root, cb))
.toArray(Predicate[]::new);
return cb.or(predicates);
}
}
// 스펙 구현을 위한 Factory class
public class Specs {
public static <T> Specification<T> and(Specification<T> ... specs) {
return new AndSpecification<>(specs);
}
public static <T> Specification<T> or(Specification<T> ... specs) {
return new OrSpecification<>(specs);
}
}
// interface 정의
public interface OrderRepository {
...
List<Order> findAll(Specification<Order> spec);
...
}
// 스펙을 이용해 검색을 수행하는 조회 기능 구현
@Repository
public class JpaOrderRepository implements OrderRepository {
@PersistenceContext
private EntityManager entityManager;
...
@Override
public List<Order> findAll(Specification<Order> spec) {
CriteriaBuilder cb = entityManager.getCriteriaBuilder();
CriteriaQuery<Order> criteriaQuery = cb.createQuery(Order.class);
// 검색 조건의 대상이 되는 루트를 생성
Root<Order> root = criteriaQuery.from(Order.class);
// 전달받은 스펙으로 Predicate 생성
Predicate predicate = spec.toPredicate(root, cb);
// criteriaQuery 의 조건으로 생성한 Predicate를 전달
criteriaQuery.where(predicate);
if (orders.length > 0) {
criteriaQuery.orderBy(
cb.desc(root.get(Order_.number).get(OrderNo_.number))
);
}
TypedQuery<Order> query = entityManager.createQuery(criteriaQuery);
return query.getResultList();
}
...
}
TypedQuery<Order> query = entityManager.createQuery(
"select o from Order o " +
"where o.orderer.memberId.id = :ordererId " +
"order by o.number.number desc",
Order.class);
List<Order> orders = orderRepository.findAll(spec, "number.number desc");
JPA Criteria 의 Order로 변환
// Repository 에서의 정렬 설정
if (orders.length > 0) {
criteriaQuery.orderBy(JpaQueryUtils.toJpaOrders(root, cb, orders));
}
// JpaQueryUtils => JPA Order로 변환
public static <T> List<Order> toJpaOrders(
Root<T> root, CriteriaBuilder cb, String... orders) {
if (orders == null || orders.length == 0) return Collections.emptyList();
return Arrays.stream(orders)
.map(orderStr -> toJpaOrder(root, cb, orderStr))
.collect(toList());
}
private static <T> Order toJpaOrder(
Root<T> root, CriteriaBuilder cb, String orderStr) {
String[] orderClause = orderStr.split(" ");
boolean ascending = true;
if (orderClause.length == 2 && orderClause[1].equalsIgnoreCase("desc")) {
ascending = false;
}
String[] paths = orderClause[0].split("\\.");
Path<Object> path = root.get(paths[0]);
for (int i = 1; i < paths.length; i++) {
path = path.get(paths[i]);
}
return ascending ? cb.asc(path) : cb.desc(path);
}
JPQL의 order by 절로 변환
// JPQL 쿼리
TypedQuery<Order> query = entityManager.createQuery(
"select o from Order o " +
"where o.orderer.memberId.id = :ordererId " +
JpaQueryUtils.toJPQLOrderBy("o", "number.number desc"),
Order.class);
// JpaQueryUtils => JPQL order by절로 변환
public static String toJPQLOrderBy(String alias, String... orders) {
if (orders == null || orders.length == 0) return "";
String orderParts = Arrays.stream(orders)
.map(order -> alias + "." + order)
.collect(joining(", "));
return "order by " + orderParts;
}
SortOrder sortOrder = new Ascending("name");
List<Member> members = memberRepository.findAll(specs, asc);
// SortOrder 타입을 활용한 CriteriaBuilder 생성
private static <T> Order toJpaOrder(Root<T> root, CriteriaBuilder cb, SortOrder sortOrder) {
String[] paths = sortOrder.getPath().split("\\.");
Path<Object> path = root.get(paths[0]);
for (int i = 1; i < paths.length; i++) {
path = path.get(paths[i]);
}
return sortOrder.isAscending() ? cb.asc(path) : cb.desc(path);
}
// SortOrder 타입 활용 정렬 처리
public static <T> List<Order> toJpaOrders(
Root<T> root, CriteriaBuilder cb, List<SortOrder> orders) {
if (orders == null || orders.isEmpty()) return Collections.emptyList();
return orders.stream()
.map(sortOrder -> toJpaOrder(root, cb, sortOrder))
.collect(toList());
}
setFirstResult()
와 setMaxResults()
메서드를 제공하고 있어 이 두 메서드를 이용해서 페이징을 구현할 수 있다.TypedQuery<Member> query = entityManager.createQuery(criteriaQuery);
query.setFirstResult(startRow);
query.setMaxResults(maxResults);
return query.getResultList();
List<Order> orders = orderRepository.findByOrdererId("testUser", 45, 15);
public Long countsAll() {
TypedQuery<Long> query = entityManager.createQuery(
"select count(o) from Order o", Long.class);
return query.getSingleResult();
}
public Long counts(Specification<Order> spec) {
CriteriaBuilder cb = entityManager.getCriteriaBuilder();
CriteriaQuery<Long> criteriaQuery = cb.createQuery(Long.class);
Root<Order> root = criteriaQuery.from(Order.class);
criteriaQuery.select(cb.count(root)).where(spec.toPredicate(root, cb));
TypedQuery<Long> query = entityManager.createQuery(criteriaQuery);
return query.getSingleResult();
}
import javax.persistence.EntityManager;
import javax.persistence.PersistenceContext;
import javax.persistence.TypedQuery;
import java.util.List;
@Repository
public class JpaOrderViewDao implements OrderViewDao {
@PersistenceContext
private EntityManager em;
@Override
public List<OrderView> selectByOrderer(String ordererId) {
String selectQuery =
"select new com.myshop.order.query.dto.OrderView(o, m, p) "+
"from Order o join o.orderLines ol, Member m, Product p " +
"where o.orderer.memberId.id = :ordererId "+
"and o.orderer.memberId = m.id "+
"and index(ol) = 0 " +
"and ol.productId = p.id "+
"order by o.number.number desc";
TypedQuery<OrderView> query =
em.createQuery(selectQuery, OrderView.class);
query.setParameter("ordererId", ordererId);
return query.getResultList();
}
}
import com.myshop.member.domain.Member;
import com.myshop.order.command.domain.Order;
import com.myshop.catalog.domain.product.Product;
public class OrderView {
private Order order;
private Member member;
private Product product;
public OrderView(Order order, Member member, Product product) {
this.order = order;
this.member = member;
this.product = product;
}
public Order getOrder() {
return order;
}
public Member getMember() {
return member;
}
public Product getProduct() {
return product;
}
}
import org.hibernate.annotations.Immutable;
import org.hibernate.annotations.Subselect;
import org.hibernate.annotations.Synchronize;
import javax.persistence.*;
import java.util.Date;
@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 {
@Id
private String number;
private long version;
private String ordererId;
private String ordererName;
private int totalAmounts;
private String receiverName;
private String state;
@Temporal(TemporalType.TIMESTAMP)
@Column(name = "orderDate")
private Date orderDate;
private String productId;
private String productName;
protected OrderSummary() {
}
... getter() ...
}
// purchase_order 테이블에서 조회
Order order = orderRepository.findById(orderNumber);
order.changeShippingInfo(newInfo); // 상태 변경
// 변경 내역이 DB에 반영되지 않았는데 purchase_order 테이블에서 조회
List<OrderSummary> summaries = ororderSunmaryRepository.findByOrdererId(userId);
purchase_order
테이블을 지정하고 있으므로 OrderSummary를 로딩하기 전에 purchase_order
테이블에 변경이 발생하면 관련 내역을 먼저 flush한다. 따라서 OrderSummary를 로딩하는 시점에서는 변경 내역이 반영 된다.// @Subselect를 적용한 @Entity는 일반 @Entity와 동일한 방법으로 조회할 수 있음.
@Override
public List<OrderSummary> selectByOrderer(String ordererId) {
TypedQuery<OrderSummary> query = em.createQuery("select os from OrderSummary " +
"os where os.ordererId = :ordererId " +
"order by os.orderDate desc", OrderSummary.class);
query.setParameter("ordererId", ordererId);
return query.getResultList();
}
select osm.number, osm.orderer_id, osm.orderer_name, osm.total_amount,
...
from (
select o.order_number as number,
o.orderer_id, o.orderer_name, o.total_amounts,
...
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
...
) osm
where osm.number = ?