도메인 주도 설계 (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개의 댓글

관련 채용 정보