시작에 앞서
CQRS는 명령(Command) 모델과 조회(Query) 모델을 분리하는 패턴이다.
검색을 위한 스펙
검색 조건을 다양하게 조합해야 할 때 사용할 수 있는 것이 스펙이다. 스펙은 애그리거트가 특정 조건을 충족하는지를 검사할 때 사용하는 인터페이스다.
스프링 데이터 JPA를 이용한 스펙 구현
스프링 데이터 JPA는 검색 조건을 표현하기 위한 인터페이스인 Specification을 제공한다.
Copy public interface Specification < T > extends Serializable {
// not, where, and, or 메서드 생략
@ Nullable
Predicate toPredicate ( Root < T > root ,
CriteriaQuery < ? > query ,
CriterialBuilder cb);
}
Copy public class OrderIdSpec implements Specification < OrderSummary > {
private String orderId;
public OrdererIdSpec ( String orderId) {
this . ordererId = oderrerId;
}
@ Override
public Predicate toPredicate ( Root < OrderSmmary > root ,
CriterialQuery < ? > query ,
CriteriaBuilder cb) {
return cb . equal ( root . get ( OrderSummary_ . ordererId ) , ordererId);
}
}
스펙 구현 클래스를 개별적으로 만들지 않고 별도 클래스에 스펙 생성 기능을 모아도 된다.
Copy public class OrderSummarySpec {
public static Specification < OrderSummary > orderId ( String ordererId) {
return ( Root< OrderSummary > root , CriteriaQuery<?> query ,
CriteriaBuilder cb) ->
cb . equal (root . < String > get( "ordererId" ) , ordererId);
}
public static Specification < OrderSummary > orderDateBetween (
LocalDateTime from , LocalDateTime to) {
return ( Root< OrderSummary > root , CriteriaQuery<?> query ,
CriteriaBuilder cb) ->
cb . between ( root . get ( OrderSummary_ . orderDate ) , from , to);
}
}
Copy Specification < OrderSummary > betweenSpec =
OrderSummarySpecs . orderDateBetween (from , to);
리포지터리/DAO에서 스펙 사용하기
스펙을 충족하는 엔티티를 검색하고 싶다면 findAll()메서드를 사용하면 된다.
Copy public interface OrderSummaryDao extends Repository < OrderSummary , String > {
List < OrderSummary > findAll ( Specification < OrderSummary > spec);
}
Copy // 스펙 객체를 생성하고
Specification< OrderSummary > spec new OrdererIdSpec( "user1" ) ;
// findAll() 메서드를 이용해서 검색
List < OrderSummary > results = orderSummaryDao . findAll (spec);
스펙 조합
Copy public interface Specification < T > extends Serializable {
default Specification < T > and (@ Nullable Specification < T > other) { ... }
default Specification < T > or (@ Nullable Specification < T > other) { ... }
@ Nullable
Predicate toPredicate ( Root < T > root ,
CriteriaQuery < ? > query ,
CriterialBuilder cb);
}
정렬 지정하기
스프링 데이터 JPA는 두 가지 방법을 사용해서 정렬을 지정할 수 있다.
1 . 메서드 이름에 OrderBy를 사용해서 정렬 기준 지정
Copy public interface OrderSummaryDao extends Repository < OrderSummary , String > {
List < OrderSummary > findByOrdererIdOrderByNumberDesc ( String ordererId);
}
findByOrdererIdOrderByNumberDesc 메서드는 다음 조회 쿼리를 생성한다.
ordererId 프로퍼티 값을 기준으로 검색 조건 지정
2 . Sort를 인자로 전달
Copy public interface OrderSummaryDao extends Repository < OrderSummary , String > {
List < OrderSummary > findByOrdererId ( String ordererId , Sort sort);
List < OrderSummary > findAll ( SPecification < OrderSummary > spec , Sort sort);
}
Copy Sort sort = SOrt . by ( "name" ) . ascending ();
List < OrderSummary > results = orderSummaryDaofindByOrdererId( "user1" , sort) l;
아래와 같이 두 개 이상의 정렬 순서를 짧게도 표현 가능
Copy Sort sort = Sort . by ( "number" ) . ascending () . and ( Sort . by ( "orderDate" ) . descending ());
페이징 처리하기
Copy public interface MemberDataDao extends Repository < MemberData , String > {
List < MemeberData > findByNameLike ( String name , Pageable pageable);
Page < MemberData > findByBlocked ( boolean blocked , Pageable pageable);
Page < MemberData > findAll ( Specification < MemberData > spec , Pageable pageable);
}
simple example PageReuqest + Sort Page가 제공하는 메서드들 more
Copy PageRequest pageReq = PageRequest . of ( 1 , 10 );
List < MemberData > user = memberDataDao . findByNameLike ( "사용자%" , pageReq);
Copy Sort sort = Sort . by ( "name" ) . descending ();
PageRequest pageReq = PafeRequest . of ( 1 , 2 , sort);
List < MemberData > user = memberDataDao . findByNameLike ( "사용자%" , pageReq);
Copy Pageable pageReq = PageRequest . of ( 2 , 3 );
Page < MemberData > page = memberDataDao . findByBlocked ( false , pageReq);
List < MemberData > content = page . getContent ();
long totalElements = page . getTotalElements ();
int totalPages = page . getTotalPages ();
int number = page . getNumber ();
int numberOfElements = pafe . getNumberOfElements ();
int size = page . getSize ();
Copy // 처음부터 N개의 데이터가 필요하다면
List< MemberData > findFirst3ByNameLikeOrderByName( String name)
스펙 조합을 위한 스펙 빌더 클래스
Before After
Copy 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 ));
Copy 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 ));
동적 인스턴스 생성
JPA는 쿼리 결과에서 임의의 객체를 동적으로 생성할 수 있는기능을 제공하고 있다.
동적 인스턴스는 JPQL을 그대로 사용하므로 객체 기준으로 쿼리를 작성하면서도 동시에 지연/즉시 로딩과 같은 고민 없이 원하는 모습으로 데이터를 조회할 수 있다.
하이버네이트 @Subselect 사용
하이버네이트는 JPA 확장 기능으로 @Subselect를 제공하는데, @Subselect는 쿼리 결과를 @Entity로 매핑할 수 있는 유용한 기능이다.
서브 쿼리를 사용하고 싶지 않다면 네이티브 SQL 쿼리를 사용하거나 마이바티스와 같은 별도 매퍼를 사용해서 조회 기능을 구현해야 한다.
Image https://dribbble.com/shots/3657142-Roller-skating-cat