CQRS란, 명령 모델과 조회 모델을 분리하는 패턴.
- 명령 모델: 상태를 변경하는 기능을 구현할 때 사용
- 조회모델은 데이터를 조회하는 기능을 구현할 때 사용
public interface Speficiation<T> {
public boolean isSatistiedBy(T agg);
// agg 파라미터는 검사 대상이 되는 객체
}
// generic type parameter T = JPA 엔티티 타입
public interface Specification<T> etends Serializable {
// not, where, and, or 메서드 생략
@Nullable
Predicate toPredicate(Root<T> root, CriteriaQuery<?> query, CriteriaBuilder cb);
}
➕ JPA 정적 메타 모델
- @StaticMetamodel 애너테이션을 이용하여 관련 모델을 지정
- 모델 클래스의 이름 뒤에
_
를 붙인 이름을 가진- 문자열로 프로퍼티를 지정할 수도 있음. 하지만 오타 가능성이 있고 코드 안정성이나 생산성 측면에서 정적 메타 모델 클래스가 유리함.
- JPA 프로바이더가 정적 메타 모델을 생성하는 도구를 제공, 사용하면 편리함
return (Root<OrderSummary> root, CriteriaQuery<?> Query, CriteriaBuilder ch) ->
cb.equal(root.<String>get("ordererId"), ordererId);
findAll()
메서드 사용public interface OrderSummaryDao extands Repository<OrderSummary, String> {
List<OrderSummary> findAll(Specification<OrderSummary> spec);
}
// 스펙 객체를 생성하고
Specification<OrderSummary> spec = new OrdererIdSpec("user1");
// findAll() 메서드를 이용하여 검색
List<OrderSummary> results = orderSummaryDao.findAll(spec);
스펙 인터페이스는 and
, or
메서드를 제공, 이 메서드들을 이용하여 스펙을 조합할 수 있음.
public interface Specification<T> extends Serializable {
...
default Specification<T> and (@Nullable specification<T> other) {
...
}
default Specification<T> other) {
...
}
@Nullable
Predicate toPredicate(Root<T> root, CriteriaQuery<?> query,
CriteriaBuilder criteriaBuilder);
}
not()
메서드도 사용할 수 있음. 정적 메서드로 조건을 반대로 적용할 때 사용
Specification<OrderSummary> spec = Specification.not(OrderSummarySpecs.ordererId("userId"));
where()
메서드를 사용하여 NullPointerException
발생 방지하기
Specification<OrderSummary> spec = Specification.where(createNullableSpec()).and(createOtherSpec());
public interface OrderSummaryDao extends REpository<OrderSummary, String> {
List<OrderSummary> findByOrdererIdOrderByNumberDesc(String ordererId);
...
// ordererId 프로퍼티 값을 기준으로 검색 조건 지정
// number 프로퍼티 값 역순으로 정렬
}
➡️ 정렬 기준 프로퍼티가 두 개 이상이면 메서드 이름이 길어짐public interface OrderSummaryDao extends Repository<OrderSummary, String> {
List<OrderSummary> findByOrdererId(String ordererId, Sort sort);
List<OrderSummary> findAll(Specification<OrderSummary> spec, Sort sort);
}
Sort sort = Sort.by("number").ascending();
Pageable
타입 파라미터: 페이징을 자동으로 처리해줌public interface MemberDataDao extends Repository<MemberData, String> {
// 마지막 메서드로 pageable 타입을 가짐.
List<MemberData> findByNameLike(String name, Pageable pageable);
}
findByNameLike()
메서드를 호출하는 예PageRequest pageReq = PageRequest.of(1, 10);
List<MemberData> user = memberDataDao.findByNameLike("사용자%", pageReq);
➕
findBy
프로퍼티 형식의 메서드는Pageable
타입을 사용하더라도 리턴 타입이List
면COUNT
쿼리를 실행하지 않으므로, 페이징 처리와 관련된 정보가 필요 없다면Page
리턴 타입이 아닌List
를 사용하기.
// if를 조합한 경우
Specification<MemberData> spec = Specification.where(null);
if (searchRequest.isOnlyNotBlocked()) {
spec = spec.and(MemberDataSpecs.nonBlocked());
}
if (StringUtils.hasText(searchRequest.getName())) {
spec = spec.and(MemberDataSpecs.nameLike(searchRequest.getName()));
}
List<MemberData> results = memberDataDao.findAll(spec, PageRequest.of(0.5));
// 스펙 빌더를 사용한 경우
Specification<MemberData> spec = SpecBuilder.builder(MemberData.class)
.ifTrue(searchRequest.isOnlyNotBlocked(),
() -> MemberDataSpecs.nonBlocked())
.ifHasText(searchRequest.getName(),
name -> MemberDataSpecs.nameLike(searchRequest.getName())
.toSpec();
List<MemberData> result = memberDataDao.findAll(spec, Pagerequest.of(0,5));
public interface OrderSummaryDao
extends Repository<OrderSummary, String> {
@Query(""
select new com.myshop.order.query.dto.OrderView(
o.number, o.state, m.name, m.id, p.name
) from Order o join o.orderLines ol. Member m, Product p
where o.orderer.memberId = :ordererId
and o.orderer.memberId.id = m.id
and index(ol) = 0
and ol.productId.id = p.id
order by o.number.number desc
"")
List<OrderView> findOrderView(String ordererId);
}
@Subselect
: 쿼리 결과를 @Entity
로 매핑할 수 있는 기능@Subselect
로 조회한 @Entity
수정 불가@Immutbale
: 엔티티의 매핑 필드/프로퍼티가 변경돼도 하이버네이트가 DB에 반영되지 않고 무시함.@Synchronize
: 엔티티와 관련한 테이블 목록을 명시함.@Entity
와 같으므로 EntityManager#find()
, JPQL
, Criteria
를 사용하여 조회할 수 있음