도메인 주도 설계 (5) - JPA 에서의 Repository 조회 기능 구현

gentledot·2021년 10월 11일
0

Repository의 조회 기능 (JPA)

검색을 위한 스팩

  • 리포지터리는 애그리거트의 저장소이다.
    • 저장하고
    • 찾고
    • 삭제하는 것이 기본 기능
  • 애그리거트를 찾을 때 식별자를 이용하는 것이 기본이지만 식별자 외에 이러 다양한 조건으로 애그리거트를 찾아야 할 때가 있다.
  • 검색 조건이 고정되어 있고 단순하면 다음과 같이 특정 조건으로 조회하는 기능을 만들면 된다.
    public interface OrderRepository {
        Order findById(OrderNo id);
        List<Order> findByOrderer(String ordererId, Date fromDate, Date toDate);
    		...
    }
  • 검색 조건의 조합이 다양해지면 모든 조합별로 flnd 메서드를 정의할 수 없다.
    • 조건 조합별로 생성되는 find 메서드가 너무 많아지기 때문이다.
    • 검색 조건의 조합이 다양할 경우 스펙(Specification)을 이용해서 문제를 풀어야 한다.
  • 스펙(Specification)은 애그리거트가 특정 조건을 충족하는지 여부를 검사한다.
    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);

JPA를 위한 스펙 구현

  • 모든 애그리거트를 조회한 다음에 스펙을 이용해서 필터링 하는 방식은 실행 속도 문제가 생길 수 있다.
    • 애그리거트 전체가 10만 개인 경우 DB에서 메모리로 로딩한 뒤에 다시 10만 개 객체를 루프 돌면서 스펙을 검사를 하게 되므로.
  • 실제 구현에서는 쿼리의 where 절에 조건을 불이서 필요한 데이터를 걸러야 한다. 즉, 스펙 구현도 메모리에서 걸러내는 방식에서 쿼리의 where 를 사용하는 방식으로 바꿔야 한다는 것이다.
    • JPA 에서는 다양한 검색 조건을 조합하기 위해 CriteriaBuilder 와 Predicate 를 사용한다.
    • JPA를 위한 스펙은 CriteriaBuilder 와 Predicate 를 이용하여 검색 조건을 구현해야 한다.

CriteriaBuilder 와 Predicate 를 이용한 스펙 구현

  • Specification interface
    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);
    }
  • OrdererSpec 구현 (orderer로 검색)
    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);
    
        }
    }
    • OrdererSpec 의 toPredicate() method는 Orderer_.memberId.id 프로퍼티가 생성자로 전달받은 ordererId 와 같은지 비교하는 Predicate 를 생성해서 리턴한다.
    • 예제에서 Order_ 클래스는 JPA의 정적 메타 모델을 정의한 코드이다.
  • 응용 서비스는 원하는 스펙을 생성하고 리포지터리에 전달해서 필요한 애그리거트 를 검색하면 된다.
    OrdererSpec spec = new OrdererSpec("ordererId");
    List<Order> orders = orderRepository.findAll(spec);
  • JPA의 정적 메타 모델
    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;
    }
    • 정적 메타 모델은 @StaticMetamodel 애노테이션을 이용해서 관련 모델을 지정한다.
    • 메타 모델 클래스는 모델 클래스의 이름 뒤에 _을 붙인 이름을 갖는다.
    • 정적 메타 모델 클래스는 대상 모델의 각 프로퍼티와 동일한 이름을 갖는 정적 필드를 정의한다. 이 정적 필드는 프로퍼티에 대한 메타 모델로서 프로퍼티 타입에 따라 SingularAttribute, ListAttribute 등 의 타입을 사용해서 메타 모델을 정의한다.
    • 정적 메타 모델을 사용하는 대신 문자열로 프로퍼티를 지정할 수도 있다.
      • root.get("orderer").get("memberld").get("id")
      • 하지만. 문자열은 오타 가능성이 있고 실행하기 전까지는 오타가 있다는 것을 놓치기 쉽다.
      • 또한, IDE 의 코드 자동 완성 기능을 사용할 수 없어 입력할 코드도 많아진다.
  • Criteria를 사용할 때에는 정적 메타 모델 클래스를 사용하는 것이 코드 안정성이나 생산성 측면에서 유리하다.
  • 정적 메타 모델 클래스를 직접 작성할 수 있지만 하이버네이트와 같은 JPA 프로바이더는 정적 메타 모델을 생성하는 도구를 제공하고 있으므로 이들 도구를 사용하면 편리하다.

And / Or 스펙 조합을 위한 구현

  • Specification의 .toPredicate() 메서드는 생성자로 전달받은 Specification 목록을 Predicate 목록으로 바꾸고 CriteriaBuilder ****의 and(), or() 를 사용해서 새로운 Predicate를 생성한다.
  • And 또는 Or 스펙에 대해 다음의 팩토리 클래스를 구성하면 스펙 생성이 간편해진다.
    // 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);
        }
    }

스펙을 사용하는 JPA 리포지터리

  • 스펙을 사용한 조회 메서드 구현
    // 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();
        }
    
    		...
    }
    • JPA의 Criteria 를 사용하는 코드 구현의 핵심은 Root 생성과 Predicate 생성, CriteriaQuery 사용에 있다.

도메인 모델의 리포지터리 구현 기술 의존

  • 도메인 모델은 구현 기술에 의존하지 않아야 한다.
  • 그런데 앞서 확인한 JPA용 Specification 인터페이스는 toPredicate() 메서드가 JPA 의 Root 와 CriteriaBuilder 에 의존하고 있으므로 사용하는 리포지터리 인터페이스는 이미 JPA에 의존하는 모양이 된다.
  • Specification 구현 기술에 대해 중립적인(추상적인) 형태로 구현해서 도메인이 구현 기술에 완전히 의존하지 않도록 해야하는가? 에 대한 저자의 대답은 "아니오" 라고 한다.
    • 리포지터리 구현 기술에 의존하지 않는 Specification 을 만들려면 많은 부분을 추상화해야 하는데 이는 많은 노력을 요구하는데 이에 비해 실제 얻는 이점은 크지 않다고 한다.
    • 리포지터리 구현 기술을 바꿀 정도의 변화는 드물기 때문이다.
    • 한 애플리케이션에서 다양한 리포지터리 구현 기술을 사용하고 각 리포지터리에 대해 동일한 스펙 인터페이스를 사용해야 하는 경우에만 스펙을 추상화하는 노력을 해야 한다고 서술되어 있다.

정렬 구현

  • JPA의 CriteriaQuery#orderBy() 를 이용해 정렬 순서를 지정한다.
    • CriteriaBuilder#asc()
    • CriteriaBuilder#desc()
  • JPQL을 사용하는 경우 JPQL의 order by 절을 사용.
    TypedQuery<Order> query = entityManager.createQuery(
    	"select o from Order o " +
        	"where o.orderer.memberId.id = :ordererId " + 
            "order by o.number.number desc",
     Order.class);
  • 정렬 순서가 고정된 경우에는 CriteriaQuery#orderBy() 나 JPQL의 order by 절을 이용해서 정렬 순서를 지정하면 되지만, 정렬 순서를 응용 서비스에 결정하는 경우에는 정렬 순서를 리포지터리에 전달할 수 있어야 한다.
  • JPA Criteria는 Order 타입을 이용해서 정렬 순서를 지정한다. 그런데 JPA의 Order는 CriteriaBuilder 를 이용해야 생성할 수 있다.
    • 정렬 순서를 지정하는 코드는 리포지터리를 사용하는 응용 서비스에 위치하게 되는데 응용 서비스는 CriteriaBuilder에 접근할 수 없다.
    • 따라서, 응용 서비스는 JPA Order가 아닌 다른 타입을 이용해서 리포지터리에 정렬 순서를 전달하고 JPA 리포지터리는 이를 다시 JPA Order 로 변환하는 작업을해야한다.

정렬 방법 1. 문자열 사용

List<Order> orders = orderRepository.findAll(spec, "number.number desc");
  • JPA 리포지터리 구현 클래스는 문자열을 파싱해서
    • JPA Criteria 의 Order로 변환하거나
    • JPQL의 order by 절로 변환
  1. 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);
    }
    • JpaQueryUtils.toJpaOrder() method를 활용, 문자열 배열로부터 JPA Order 객체를 생성한다.
      • name desc → cb.desc(root.get("name"))
      • customer.name asc → cb.asc(root.get("customer").get("name"))
  2. 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;
    }

정렬방법 2. 타입으로 구현

  • JpaQueryUtils 클래스의 정렬 순서를 처리하는 구현은 문자열 처리가 왼벽하지 않다.
    • 예를 들어. toJpaOrder() 메서드는 desc 경우에 내림차순으로 설정하고. 나머지 문자열인 경우 오름차순으로 처리한다.
    • 'des', 'ascending' 등과 같이 잘못된 문자열을 사용해도 오름차순으로 정릴한다. 이런 문제를 조금이나마 해소하려면 다음과 같이 정렬 순서를 타입으로 표현해서 전달하면 된다.
      SortOrder sortOrder = new Ascending("name");
      List<Member> members = memberRepository.findAll(specs, asc);
  • SortOrder는 isAscending() 과 같은 메서드를 제공하고, JpaQueryUtils 클래스는 이 메서드를 이용해서 알맞게 정렬을 처리한다.
    // 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());
    }

페이징과 개수 구하기 구현

  • JPA 쿼리 (TypedQuery)는 setFirstResult()setMaxResults() 메서드를 제공하고 있어 이 두 메서드를 이용해서 페이징을 구현할 수 있다.
    TypedQuery<Member> query = entityManager.createQuery(criteriaQuery);
    query.setFirstResult(startRow);
    query.setMaxResults(maxResults);
    return query.getResultList();
    • setFirstResult() 메서드는 읽어올 첫 번째 행 번호를 지정한다.
      • 첫 행은 0번부터 시작 한다.
    • setMaxResults() 메서드는 읽어올 행 개수를 지정한다.
    • 만약 한 페이지에 보여줄 행 개수가 15개이고 보여줄 페이지 번호가 4라면, 4페이지의 첫 번째 행은 46번째 행 이므로 시작 행 번호 값은 45가 된다.
      List<Order> orders = orderRepository.findByOrdererId("testUser", 45, 15);
  • 페이징과 함께 사용되는 기능이 전체 개수를 구하는 기능이다. 전체 개수를 구하는 기능은 JPQL을 이용해서 간단하게 구현할 수 있다.
    public Long countsAll() {
        TypedQuery<Long> query = entityManager.createQuery(
    				"select count(o) from Order o", Long.class);
        return query.getSingleResult();
    }
  • Specification을 이용해서 특정 조건을 충족하는 애그리거트 개수를 구할 때는 다음과 같이 코드를 작성할 수 있다.
    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();
    }

Spring Data JPA의 활용

조회 전용 기능 구현

  • 리포지터리는 애그리거트의 저장소를 표현하는 것으로서 다음 용도로 리포지터리 를 사용하는 것은 적합하지 않다.
    • 여러 애그리거트를 조합해서 한 화면에 보여주는 데이터 제공
      • JPA의 지연 로딩과 즉시 로딩 설정, 연관 매핑 설정을 고려해야 한다.
      • 애그리거트 간 직접 연관을 맺는 경우 ID 참조할 때의 장점을 활용할 수 없음.
    • 각종 통계 데이터 제공
      • 다양한 테이블을 조인하거나 DBMS 전용 기능 사용을 JPQL이나 Criteria 로 처리하기 힘듦.
  • 애초에 이런 기능은 조회 전용 쿼리로 처리해야 하는 것들이다. JPA, Hibernate 를 사용하면 아래의 방법으로 조회 전용 기능을 구현할 수 있다.
    • 동적 인스턴스 생성
    • 하이버네이트의 @Subselect 확장 기능
    • 네이티브 쿼리를 이용

동적 인스턴스 생성

  • JPA는 쿼리 결과에서 임의의 객체를 동적으로 생성할 수 있는 기능을 제공한다.
    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();
        }
    }
    • 이 코드에서 JPQL의 select 절을 보면 new 키워드가 있다. new 키워드 뒤에 생성할 인스턴스의 완전한 클래스 이름을 지정하고 괄호 안에 생성자에 인자로 전달할 값을 지정한다.
    • 이 코드의 경우 OrderView 생성자에 인자로 각각 Order, Member, Product를 전달하고 생성자는 전달받은 객체로부터 필요한 값을 추출한다.
      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;
          }
      }
    • 조회 전용 모델을 만드는 이유는 표현 영역을 통해 사용자에게 데이터를 보여주기 위함이다. 많은 웹 프레임워크는 새로 추가한 밸류 타입을 알맞은 형식으로 출력하지 못하므로 위 코드처 럼 값을 기본 타입으로 변환하면 편리하다.
    • 물론, 사용하는 웹 프레임위크에 익숙하다면 원하는 형식으로 출력하도록 프레임워크를 확장해서 조회 전용 모델에서 밸류 타입의 의미가 사라지지 않도록 할 수 있다.
    • 모델의 개별 프로퍼티를 생성자에 전달할 수도 있다. 주문 목록을 보여줄 목적으로 OrderView를 사용한다면 생성자로 필요한 값만 전달받아도 될 것이다. (ValueType의 필요한 컬럼만 전달하는 것도 가능)
    • 동적 인스턴스의 장점은 JPQL을 그대로 사용하므로 객체 기준으로 쿼리를 작성하면서도 동시에 지연, 즉시 로딩과 같은 고민 없이 원하는 모습으로 데이터를 조회할 수 있다는 점이다.

Hibernate 의 @Subselect 사용

  • 하이버네이트는 JPA 확장 기능으로 @Subselect 를 제공한다. @Subselect 는 쿼리 결과를 @Entity로 매핑할 수 있는 유용한 기능이다.
    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() ...
    
    }
    • @Immutable, @Subselect, @Synchronize는 hibernate 전용 annotation으로 해당 태그로 테이블이 아닌 쿼리 결과를 @Entity 로 매핑할 수 있음.
    • @Subselect 는 조회(select)쿼리를 값으로 갖는다. 하이버네이트는 이 select 쿼리의 결과를 매핑할 테이블처럼 사용한다.
    • DBMS가 여러 테이블을 조인해서 조회한 결과를 한 테이블처럼 보여주기 위한 용도로 View를 사용하는 것과 같음
    • View를 수정할 수 없듯이 @Subselect로 조회한 @Entity 역시 수정할 수 없다.
      • @Subselect를 이용한 @Entity의 매핑 필드를 수정하면 하이버네이트는 변경 내역을 반영하는 update 쿼리를 실행할 것이다.
      • 그런데, 매핑한 테이블이 없으므로 에러가 발생한다.
      • @Immutable 을 사용하면 하이버네이트는 해당 엔티티의 매핑 필드/프로퍼티가 변경되어도 DB에 반영하지 않고 무시한다.
  • Order 상태를 변경한 뒤에 OrderSummary를 조회하는 경우
    // purchase_order 테이블에서 조회 
    Order order = orderRepository.findById(orderNumber);
    order.changeShippingInfo(newInfo); // 상태 변경
    
    // 변경 내역이 DB에 반영되지 않았는데 purchase_order 테이블에서 조회
    List<OrderSummary> summaries = ororderSunmaryRepository.findByOrdererId(userId);
    • 특별한 이유가 없으면 하이버네이트는 변경사항을 트랜잭션을 커밋하는 시점에 DB에 반영하므로, Order의 변경 내역이 purchase_order 테이블에 반영하지 않은 상태이므로 OrderSummary에는 최신 값이 아닌 이전 값이 담기게 된다.
    • 이런 문제를 해소하기 위한 용도로 사용한 것이 @Synchronize 이다. @Synchronize 는 해당 엔티티와 관련된 테이블 목록을 명시한다.
    • 하이버네이트는 엔티티를 로딩하기 전에 지정한 테이블과 관련된 변경이 발생하면 flush를 먼저한다. OrderSummary의 @Synchronize 는 purchase_order 테이블을 지정하고 있으므로 OrderSummary를 로딩하기 전에 purchase_order 테이블에 변경이 발생하면 관련 내역을 먼저 flush한다. 따라서 OrderSummary를 로딩하는 시점에서는 변경 내역이 반영 된다.
  • @Subselect 를 사용해도 일반 @Entity와 같기 때문에 EntityManager=find(), JPQL, Criteria 를 사용해서 조회할 수 있다는 것이 @Subselect의 장점이다. (스펙을 사용한 수 있다는 것도 포함)
    // @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();
    }
  • @Subselect의 값으로 지정한 쿼리를 from 절의 서브쿼리로 사용한다. 실행하는 쿼리는 다음과 같은 형식을 갖는다.
    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 = ?
    • 서브쿼리를 사용하고 싶지 않다면 native SQL query를 사용하거나 MyBatis와 같은 별도 매퍼를 사용해 조회 기능을 구현해야 한다.
profile
그동안 마신 커피와 개발 지식, 경험을 기록하는 공간

0개의 댓글