# 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 사용해서 조회 기능 구현해야 함