# 5. 스프링 데이터 JPA를 이용한 조회 기능

# 5.1 시작에 앞서

# CQRS

  • 명령(command) 모델과 조회(Query) 모델을 분리하는 패턴
    • 명령 : 상태를 변경하는 기능 구현
    • 조회 : 데이터를 조회하는 기능 구현

# 5.2 검색을 위한 스펙

# 스펙

  • 검색 조건을 다양하게 조합해야 할 때 사용
  • 애그리거트가 특정 조건을 충족하는지를 검사할 때 사용하는 인터페이스
public interface Specification<T> {
    public boolean isSatisfiedBy(T agg);
}
  • 검사 대상 객체가 조건을 충족하면 true, 아니면 false
public List<Order> findAll(Specification<Order> spec) {
    List<Order> allOrders = findAll();
    return allOrders.stream()
                    .filter(order -> spec.isSatisfiedBy(order))
                    .toList();
}
  • 위 코드와 같이 검색 대상을 걸러내는 용도로 사용
  • 근데 실제 스펙은 이렇게 구현하지 않음 → 메모리, 조회 성능 문제

# 5.3 스프링 데이터 JPA를 이용한 스펙 구현

스프링 데이터 JPA는 검색 조건을 표현하기 위한 Specification 인터페이스가 존재함

public static Specification<PointLog> hasUserId(String userId) {
    return (root, query, cb) -> cb.equal(root.get("member").get("userId"), userId);
}

대신

@StaticMetamodel(PointLog.class)
public class PointLog_ {
    public static volatile SingularAttribute<PointLog, String> userId;
}

public static Specification<PointLog> hasUserId(String userId) {
    return (root, query, cb) -> cb.equal(root.get("member").get("userId"), userId);
}

이렇게 정적 메타 모델을 사용하면 컴파일 시 오류 발견 가능

별도 클래스에 스펙 생성 기능을 모아도 굿

# 5.4 리포지터리 / DAO에서 스펙 사용하기

  • findAll(spec) 으로 스펙을 적용해서 검색 가능

# 5.5 스펙 조합

  • and, or 로 스펙 조합 가능
Specification<PointLog> combinedSpec = spec1.and(spec2);
  • Specification.not() 으로 반대로 적용도 가능

# 5.6 정렬 지정하기

# 스프링 데이터 JPA에서 정렬 지정하는 법

  • 메서드 이름에 OrderBy 사용해서 정렬 기준 지정
  • Sort를 인자로 전달
List<OrderSummay> findByOrdererIdOrderByNumberDesc(String ordererId);
  • findByOrdererIdOrderByOrderDateDescNumberAsc → 이런건 너무 김

# Sort 타입 사용

Sort sort = Sort.by("number").ascending();
List<OrderSummary> results = orderSummaryDao.findByOrderId("user1", sort);
  • and로 연결 가능
Sort combinedSort = sort1.and(sort2);

# 5.7 페이징 처리하기

  • PageRequest 클래스로 호출 가능
PageRequest pageReq = PageRequest.of(1, 10);
List<MemberData> user = memberDataDao.findByNameLike("사용자%", pageReq);
  • sort도 지정 가능
Sort sort = Sort.by("name").descending();
PageRequest pageReq = PageRequest.of(1, 10, sort);
List<MemberData> user = memberDataDao.findByNameLike("사용자%", pageReq);
  • 페이징 쿼리는 전체 개수, 페이지 개수 등 페이지 처리에 필요한 데이터도 함꼐 제공
  • 만약에 이런게 필요없으면 리턴 타입으로 Page 대신 List 사용하기
  • Page 관련 성능개선 : https://jojoldu.tistory.com/530

# 5.8 스펙 조합을 위한 스펙 빌더 클래스

# EX

Specification<Check> spec = SpecBuilder.builder(Check.class)
        .and(CheckSpecs.yearQuarter(searchRequest.getYear(), searchRequest.getQuarter()))
        .ifHasText(searchRequest.getTeamCd(), str -> CheckSpec.teamCd(str))
        .ifHasText(searchRequest.getPlanDate(), CheckSpec::planDate)
        .toSpec();

List<Check> checks = checkRepository.findAll(specs);

# SpecBuilder.class

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<>();

        private void addSpec(Specification<T> spec) {
            if (spec != null) {
                specs.add(spec);
            }
        }

        public Builder<T> and(Specification<T> spec) {
            addSpec(spec);
            return this;
        }

        public Builder<T> ifHasText(String str,
                                    Function<String, Specification<T>> specSupplier) {
            if (StringUtils.hasText(str)) {
                addSpec(specSupplier.apply(str));
            }
            return this;
        }

        public Builder<T> ifTrue(Boolean cond,
                                 Supplier<Specification<T>> specSupplier) {
            if (cond != null && cond.booleanValue()) {
                addSpec(specSupplier.get());
            }
            return this;
        }

        public <V> Builder<T> ifNotNull(V value,
                                        Function<V, Specification<T>> specSupplier) {
            if (value != null) {
                addSpec(specSupplier.apply(value));
            }
            return this;
        }

        public Specification<T> toSpec() {
            Specification<T> spec = Specification.where(null);
            for (Specification<T> s : specs) {
                spec = spec.and(s);
            }
            return spec;
        }
    }
}

# 5.9 동적 인스턴스 생성

  • JPA는 쿼리 결과에서 임의의 객체를 동적으로 생성할 수 있는 기능이 있음
@Query("""
			select new com.myshop.order.query.dto.OrderView(
				o.number, o.state, m.name, m.id, p.name
			)
			from ~
			...
""")
List<OrderView> findOrderView(String ordererId);
  • JPQL을 그대로 사용하므로 객체 기준으로 쿼리를 작성하면서도, 동시에 지연/즉시로딩과 같은 고민 없이 원하는 모습으로 데이터를 조회할 수 있음
  • 영속성 컨텍스트에 의해 관리되지 않지만 조회 전용이므로 괜찮음

# 5.10 하이버네이트 @Subselect 사용

  • 하이버네이트는 JPA 확장 기능으로 @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() {
    }
 
    ...
}
  • 쿼리 실행 결과를 매핑할 테이블처럼 사용
  • 수정 불가능
  • @SubSelect를 이용한 @Entity 수정 시 update 쿼리 발생 → 매핑 한 테이블 없음 → 에러 발생
  • 그래서 @Immutable 사용해서 DB에 반영 무시
  • @Synchronize 쓰면 명시된 테이블 목록을 참고해서 변경 반영
  • @SubSelect를 사용하면 일반 @Entity 처럼 spec, page 등도 가능
  • @SubSelect 의 값으로 지정한 부분이 from 절로 들어감 → 서브쿼리 사용하고 싶지 않으면 네이티브 sql 사용 or 마이바티스와 같은 mapper 사용해서 조회 기능 구현해야 함