도메인 주도 개발 시작하기: 5. 스프링 데이터 JPA를 이용한 조회 기능

ParkIsComing·2023년 3월 27일
0

5장은 대부분 spec을 이용하여 설명하고 있다.

처음 들어본 개념이라서 찾아보니 Specification과 Querydsl이 비교 대상으로 자주 언급된다.
결론적으로 Specification의 사용은 지양하고, Querydsl을 사용하는 것 같다.
그래서 이번 포스팅에서는 Specificaton과 Querydsl의 개념을 모두 정리해보고, 둘을 비교해보는 내용 위주로 작성하고자 한다.

Specification VS Querydsl

🔻한번씩 읽어보면 좋을 것 같다.

Spring blog에서 비교한 글
https://spring.io/blog/2011/04/26/advanced-spring-data-jpa-specifications-and-querydsl
김영한님의 답변
https://www.inflearn.com/questions/16685/specification-%EB%8C%80%EB%B9%84-querydsl%EC%9D%98-%EC%9E%A5%EC%A0%90%EC%9D%B4-%EC%96%B4%EB%96%A4%EA%B2%83%EC%9D%B4-%EC%9E%88%EC%9D%84%EA%B9%8C%EC%9A%94

1) Specificaton

스프링 데이터 JPA는 Specification 인터페이스를 다음과 같이 정의하고 있다.

public interface Specification<T> {
  Predicate toPredicate(Root<T> root, CriteriaQuery query, CriteriaBuilder cb);
}

이런 식으로 스펙 생성 기능을 별도 클래스에 모아 사용할 수 있다.

public CustomerSpecifications {

  public static Specification<Customer> customerHasBirthday() {
    return new Specification<Customer> {
      public Predicate toPredicate(Root<T> root, CriteriaQuery query, CriteriaBuilder cb) {
        return cb.equal(root.get(Customer_.birthday), today);
      }
    };
  }

  public static Specification<Customer> isLongTermCustomer() {
    return new Specification<Customer> {
      public Predicate toPredicate(Root<T> root, CriteriaQuery query, CriteriaBuilder cb) {
        return cb.lessThan(root.get(Customer_.createdAt), new LocalDate.minusYears(2));
      }
    };
  }
}

레포지토리에서는 JpaSpecificationExecutor를 상속한다.

public interface CustomerRepository extends JpaRepository<Customer>, JpaSpecificationExecutor {
  // Your query methods here
}

그러면 다음과 같이 findAll()을 사용해 스펙을 만족하는 엔티티를 검색할 수 있다.

customerRepository.findAll(hasBirthday());
customerRepository.findAll(isLongTermCustomer());

💥참고: Criteria API

  • JPQL (Java Persistence Query Language) 조회를 위한 문자열을 빌드하는 대신 Java™ 오브젝트를 사용하여 조회를 빌드하기 위한 API
  • 문자 기반의 JPQL보다 동적 쿼리를 안전하게 생성할 수 있으나, 직관적으로 이해하기 어렵다는 단점
  • CriteriaBuilder에서 Criteria query를 생성한다.
  • 조회의 시작점을 뜻하는 Root객체를 이용해 값을 얻는다. Root 객체의 변수명 m은 JPQL에서 별칭이라고 생각하면 된다.

2) Querydsl

  • JPQL에서 지원하지 않는 쿼리는 QueryDSL에서 작성할 수 없음
    • UNION 지원 x
    • FROM절에서 서브쿼리 x
  • QueryDslPredicateExecutor 또는 QueryDslRepositorySupport를 사용하여 구현

QueryDslPredicateExecutor

  • 다음과 같이 repository에 QueryDslPredicateExecutor을 상속하여 사용할 수 있다.
  • 메소드에 Predicate을 인자로 넣어 쿼리를 수행한다.
  • JOIN, FETCH를 사용할 수 없다.( JPQL에서의 묵시적 조인은 가능)
public interface QuerydslPredicateExecutor<T> {

  Optional<T> findById(Predicate predicate);  (1)

  Iterable<T> findAll(Predicate predicate);   (2)

  long count(Predicate predicate);            (3)

  boolean exists(Predicate predicate);        (4)

  // … more functionality omitted.
}
interface UserRepository extends CrudRepository<User, Long>, QuerydslPredicateExecutor<User> {
}

QueryDslRepositorySupport

  • QueryDLS 의 다양한 기능을 모두 사용하려면 QueryDslRepositorySupport를 사용해야 한다.
  1. JpaRepository를 상속받는 인터페이스를 만든다.
  2. custom 메서드를 정의하는 repository 인터페이스를 만든다.
  3. 2번의 인터페이스를 구현하는 Impl클래스를 만들고, QueryDslRepositorySupport를 상속하도록 한다. 생성자에서super(EntityType.class)를 호출한다. 메서드를 구현할 때 JOIN, FETCH 사용 가능하다.
  4. 1번에서 만든 인터페이스에서 2번에서 만든 인터페이스를 상속하게 한다.
@Repository
public interface UserRepository extends JpaRepository<User,Long>, CustomerUserRepository {
	Optional<User> findByName(String name);
}
public interface CustomUserRepository {
	List<User> findAllUserByAge(Integer age);

}
public class UserRepositoryImpl extends QueryDslRepositorySupport implements CustomUserRepository{
	public UserRepositoryImpl(){
    	super(User.class);
    }
    
    @Override
    public List<User> findAllUserByAge(Integer age){
    	QUser user = QUser.user;
        return from(user)
        		.where(user.age.eq(23L))
                .fetch();
    }
}

5.6 정렬 지정하기

스프링 데이터 JPA를 이용하면 두가지 방법으로 정렬을 지정할 수 있다.
1. 메서드 이름에 OrderBy 사용

  • Desc, Asc 설정 가능
  • 두 개 이상의 프로퍼티에 대한 정렬 순서 지정도 가능
  • ex) findByOrdererIdOrderByOrderDateDescNumberAsc
  1. Sort를 인자로 전달
    JpaRepository를 상속받아 사용하는 경우
    findAll에 결과에 대한 sort
List<Entity> list명 = repository명.findAll(Sort.by(Sort.Direction.DESC/ASC, "기준컬럼명"));

5.7 페이징 처리

PageableDefault 인터페이스

적용 예시

5.9 동적 인스턴스 생성

쿼리 결과에서 임의의 객체를 동적으로 생성할 수 있다.

아래 코드에서는 OrderView 객체를 new 키워드를 이용해 동적으로 생성한다.

public interface OrderSummaryDao extends Repository<OrderSummary,String>{
	@Query("select new com.myship.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.id = m.id
    	and index(ol) = 0
        and ol.productId.id = p.id
        order by o.number.number desc")
    List<OrderView> findOrderView(String ordererId);
}

5.10 하이버네이트 @Subselect 사용

@Subselect

  • 쿼리 결과를 @Entity로 맵핑할 수 있다. 뷰처럼 사용할 수 있다.

@Immutable

  • 하이버네이트가 해당 엔티티의 매핑 필드/프로퍼티가 변경되어도 db에 반영하지 않고 무시한다.

@Synchronize

  • 해당 엔티티와 관련된 테이블 목록을 명시한다.
import org.hibernate.annotations.Immutable;
import org.hibernate.annotations.Subselect;
import org.hibernate.annotations.Synchronize;

import javax.persistence.Column;
import javax.persistence.Entity;
import javax.persistence.Id;

@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;

    @Column(name="orderer_id")
    private String ordererId;

    @Column(name="orderer_name")
    private String ordererName;

    protected OrderSummary(){

    }
}

0개의 댓글