# 05.Spring Data JPA를 이용한 조회 기능

## 사작에 앞서

CQRS(Command and Query Responsibility Segregation)

* 명령 모델과 조회 모델을 분리하는 패턴
* `명령 모델`은 상태를 변경하는 기능을 구현할 때 사용(도메인 모델)
* `조회 모델`은 데이터를 조회하는 기능을 구현할 때 사용(조회 모델)

조회 모델을 구현할 때 **JPA, myBatis, jdbcTemplate 등 다양한 기술이 사용**된다.

* 모든 DB 연동 코드를 JPA만 사용해서 구현해야 한다고 생각하지 말자.

## 검색을 위한 스펙

`스펙`(Specification)

* 검색 조건을 다양하게 조합해야 할 때 사용할 수 있는 것
* 애그리거트가 특정 조건을 충족하는지를 검사할 때 사용하는 인터페이스

스펙 인터페이스 정의

```java
public interface Specification<T> {
    public boolean isSatisfiedBy(T agg);
}
```

스펙을 **리포지터리에 사용하면 agg(검사 대상 객체)는 애그리거트 루트**가 되고, 스펙을 **DAO에 사용하면 agg는 검색 결과**로 리턴할 데이터 객체가 된다.

isSatisfiedBy() 메서드는 검사 대상 객체가 조건을 충족하면 true, 그렇지 않으면 false 리턴

```java
public class OrdererSpec implements Specification<Order> {
	private String ordererId;
    ...

	public boolean isSatisfiedBy(Order agg) {
		return agg.getOrdererId().getMemberId().getId().equals(ordererId);
	}
}
```

리포지터리나 DAO는 **검색 대상을 걸러내는 용도**로 스펙을 사용

```java
public class MemoryOrderRepository implements OrderRepository {
    ...
    public List<Order> findAll(Specification<Order> spec) {
        List<Order> allOrders = findAll();
        return allOrders.stream()
                        .filter(order -> spec.isSatisfiedBy(order))
                        .toList();
    }
}
```

리포지터리가 스펙을 이용해서 검색 대상을 걸러주므로 특정 조건을 충족하는 애그리거트를 찾고 싶으면 **원하는 스펙을 생성해서 리포지터리에 전달**해 주기만 하면 된다.

* 실제 스펙은 이렇게 구현하지 않고, Spring Data JPA를 이용

```java
Specification<Order> ordererSpec = new OrdererSpec("madvirus");
List<Order> orders = orderRepository.findAll(ordererSpec);
```

## Spring Data JPA를 이용한 스펙 구현

Spring Data JPA는 검색 조건을 표현하기 위한 인터페이스인 `Specification` 제공

* 제네릭 타입 파라미터 `T`는 **JPA 엔티티 타입**
* `toPredicate()` 메서드는 JPA Criteria API에서 조건을 표현하는 **Predicate 생성**

```java
package org.springframework.data.jpa.domain;

public interface Specification<T> extends Serializable {
    ...

    @Nullable
    Predicate toPredicate(Root<T> root, 
                          CriteriaQuery<?> query, 
                          CriteriaBuilder criteriaBuilder);
}
```

엔티티 타입이 **OrderSummary**이고, **orderId** 프로퍼티 값이 지정한 값과 동일한 스펙

* OrderSummary에 대한 검색 조건 표현
* toPredicate() 메서드는 ordererId 프로퍼티 값이 생성자로 받은 ordererId와 동일한지 비교하는 Predicate 생성
* `OrderSummary_`는 JPA 정적 메타 모델.
  * 하이버네이트와 같은 JPA 프로바이더는 정적 메타 모델을 생성하는 도구를 제공

```java
public class OrdererIdSpec implements Specification<OrderSummary> {
	private String ordererId;
    ...

    @Override
	public Predicate toPredicate(Root<OrderSummary> root, 
                                CriteriaQuery<?> query, 
                                CriteriaBuilder cb) {
		return cb.equal(root.get(OrderSummary_.ordererId), ordererId);
	}
}
```

스펙 구현 클래스를 개별적으로 만들지 않고, **별도 클래스에 스펙 생성 기능을 모아**도 된다.

* 스펙 인터페이스는 함수형 인터페이스이므로 람다식 이용도 가능

```java
public class OrderSummarySpecs {
    public static Specification<OrderSummary> ordererId(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);
    }
}

...

Specification<OrderSummary> betweenSpec = OrderSummarySpecs.orderDetailBetween(from, to);
```

## Repository/DAO에서 스펙 사용하기

스펙을 충족하는 엔티티를 검색하고 싶다면, `findAll()` 메서드를 사용

* 스펙 인터페이스를 파라미터로 갖음

```java
public interface OrderSummaryDao extends Repository<OrderSummary, String> {
    List<OrderSummary> findAll(Specification<OrderSummary> spec);
    List<OrderSummary> findAll(Specification<OrderSummary> spec, Sort sort);
    List<OrderSummary> findAll(Specification<OrderSummary> spec, Pageable pageable);
    ...
}

...

Specification<OrderSummary> spec = new OrdererIdSpec("user1"); // 스펙 객체 생성
List<OrderSummaryDao> results = orderSummaryDao.findAll(spec); // findAll() 메서드를 이용한 검색
```

## 스펙 조합

Spring Data JPA 스펙 인터페이스는 스펙을 조합할 수 있는 두 메서드(`and`, `or`)를 제공

* 기본 구현을 가진 디폴트 메서드

```java
public interface Specification<T> extends Serializable {
    default Specification<T> and(@Nullable Specification<T> other) {
        return SpecificationComposition.composed(this, other, CriteriaBuilder::and);
    }

    default Specification<T> or(@Nullable Specification<T> other) {
        return SpecificationComposition.composed(this, other, CriteriaBuilder::or);
    }
    ...
}

...

Specification<OrderSummary> spec = OrderSummarySpecs.ordererId("user1")
        .and(OrderSummarySpecs.orderDateBetween(from, to));
```

`not()` 메서드는 정적 메서드로 조건을 반대로 적용할 때 사용

```java
Specification<OrderSummary> spec = 
            Specification.not(OrderSummarySpecs.ordererId("user1"));
```

`where()` 메서드는 정적 메서드로 null을 전달하면 아무 조건도 생성하지 않는 스펙 객체를 리턴하고, null이 아니면 인자로 받은 스펙 객체를 그대로 리턴

```java
Specification<OrderSummary> spec = 
            Specification.where(createNullableSpec()).and(createOtherSpec());
```

## 정렬 지정하기

Spring Data JPA는 두 가지 방법을 사용해서 **정렬 지정** 가능

* 메서드 이름에 OrderBy를 사용해서 정렬 기준 지정
* Sort를 인자로 전달

```java
List<OrderSummary> findByOrdererIdOrderByNumberDesc(String ordererId);
```

두 개 이상의 프로퍼티에 대한 정렬 순서를 지정할 경우

```java
List<OrderSummary> findByOrdererIdOrderByOrderDateDescNumberAsc(String ordererId);
```

메서드 이름에 OrderBy를 사용하는 방법은 간단하지만 정렬 기준의 프로퍼티가 두 개 이상이면 메서드 이름이 길어지는 단점이 존재하고, 상황에 따라 정렬 순서를 변경할 수도 없음.

이럴 경우, `Sort` 타입을 적용할 수 있다.

```java
List<OrderSummary> findByOrdererId(String ordererId, Sort sort);
List<OrderSummary> findAll(Specification<OrderSummary> spec, Sort sort);

...

Sort sort = Sort.by("number").ascending();
List<OrderSummary> results = orderSummaryDao.findByOrdererId("user1", sort);

Sort sort = Sort.by("number").ascending().and(Sort.by("orderDate").descending());
```

## 페이징 처리하기

Spring Data JPA는 **페이징 처리를 위해 Pageable 타입**을 이용

* find 메서드에 Pageable 타입 파라미터를 전달하면 페이징을 자동으로 처리
* Pageable 타입 객체는 PageRequest 클래스를 이용하여 생성
* 페이지 번호는 0번부터 시작
* 리턴 타입으로 Page 타입을 사용하면 데이터 목록과 함께 페이징 처리에 필요한 데이터도 함께 제공

```java
List<OrderSummary> findByOrdererId(String ordererId, Pageable pageable);
List<OrderSummary> findAll(Specification<OrderSummary> spec, Pageable pageable);
Page<OrderSummary> findAll(Pageable pageable);

...

PageRequest pageReq = PageRequest.of(1, 10); // 페이지 번호, 한 페이지의 개수 (11번째~20번째 데이터)
List<MemberData> user = memberDataDao.findByNameLike("사용자%", pageReq);
```

PageRequest, Sort 조합도 가능

```java
Sort sort = Sort.by("name").descending();
PageRequest pageReq = PageRequest.of(1, 2, sort);
List<MemberData> user = memberDataDao.findByNameLike("사용자%", pageReq);
```

> 프로퍼티를 비교하는 findBy프로퍼티 형식의 메서드는 Pageable 타입을 사용하더라도 리턴 타입이 List면 COUNT 쿼리를 실행하지 않는다.
>
> 페이징 처리와 관련된 정보가 필요 없다면 리턴 타입을 List로 사용해서 불필요한 COUNT 쿼리를 실행하지 않도록 하고, 페이징 처리가 필요할 때 리턴 타입을 Page로 사용하자.
>
> 반면, 스펙을 사용하는 findAll 메서드에 Pageable 타입을 사용하면 리턴 타입이 Page가 아니더라도 COUNT 쿼리를 실행한다. 이 경우 COUNT 쿼리를 실행하고 싶지 않다면 커스텀 리포지터리 기능을 이용해 보자.
>
> 참고. <https://javacan.tistory.com/entry/spring-data-jpa-range-query>

N개의 데이터만 조회할 경우

* First, Top 뒤에 숫자가 없을 경우 한 개의 결과만 리턴

```java
findFirst3ByNameLikeOrderByName(String name);
findFirstByBlockedOrderById(boolean blocked);
```

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

스펙 빌더를 사용하면 **조건에 따라 스펙을 조합**할 때 편리하다.

* if 블록을 사용하지 않으면서 코드 가독성을 높이고 구조를 단순하게 할 수 있다.

```java
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));
```

[스펙 빌더 클래스](https://github.com/jihunparkme/ddd-start2/blob/main/src/main/java/com/myshop/common/jpa/SpecBuilder.java)

## 동적 인스턴스 생성

JPA는 **쿼리 결과에서 임의의 객체를 동적으로 생성할 수 있는 기능**을 제공

* 조회 전용 모델을 만드는 이유는 표현 영역을 통해 사용자에게 데이터를 보여주기 위함
* 새로 추가한 밸류 타입을 알맞는 형식으로 출력하지 못하므로 값을 기본 타입으로 변환하면 편리

```java
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.id = :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);
}

...

public class OrderView {

    private final String number;
    private final OrderState state;
    private final String memberName;
    private final String memberId;
    private final String productName;

    public OrderView(OrderNo number, OrderState state, String memberName, MemberId memberId, String productName) {
        this.number = number.getNumber();
        this.state = state;
        this.memberName = memberName;
        this.memberId = memberId.getId();
        this.productName = productName;
    }
    ...
}
```

> 동적 인스턴스의 장점은 JPQL을 그대로 사용하므로 객체 기준으로 쿼리를 작성하면서도 동시에 지연/즉시 로딩과 같은 고민 없이 원하는 모습으로 데이터를 조회할 수 있다는 점이다.

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

하이버네이트는 JPA 확장 기능으로 `@Subselect`를 제공\`

* 쿼리 결과를 `@Entity`로 매핑할 수 있는 유용한 기능

```java
@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;
    ...
```

`@Immutable`, `@Subselect`, `@Synchronize`는 하이버네이트 전용 애노테이션인데, 이 태그로 테이블이 아닌 **쿼리 결과를 @Entity로 매핑** 가능하다.

`@Subselect`

* 조회 쿼리를 값으로 갖는다.
* select 쿼리 결과를 매핑할 테이블처럼 사용(DBMS의 뷰와 유사)
* 뷰와 동일하게 `@Subselect`로 조회한 `@Entity`는 수정 불가

`@Immutable`

* `@Subselect`를 이용한 `@Entity`의 매핑 필드 수정 시 하이버네이트가 변경 내역을 반영하여 **update 쿼리를 실행하는 문제를 방지**하기 위해 사용
* 매핑 테이블이 없으므로 에러가 발생하는데 이 문제를 방지
* `@Immutable`를 사용하면 해당 엔티티의 매핑 필드/프로퍼티가 변경되어도 DB에 반영하지 않고 무시

`@Synchronize`

* 엔티티 상태 변경 내역이 아직 테이블이 반영되지 않은 상태(커밋 전 시점)에서 조회 시 **최신 값이 아닌 이전 값이 담기는 문제를 해소**하기 위해 사용
* 해당 엔티티와 관련된 테이블 목록 명시
* 하이버네이트는 엔티티 로딩 전에 지정한 테이블과 관련된 변경이 발생하면 flush 수행

.

`@Subselect`를 사용해도 일반 `@Entity`와 같으므로 EntityManager#find(), JPQL, Criteria, Spec을 사용한 조회가 가능

```java
Specification<OrderSummary> spec = OrderSummarySpecs.orderDateBetween(from, to);
Pageable pageable = PageRequest.of(1, 1);
List<OrderSummary> results = orderSummaryDao.findAll(spec, pageable);
```

단, `@Subselect`는 지정 쿼리를 **from 절의 서브 쿼리로 사용**하는데, 서브 쿼리를 사용하고 싶지 않을 경우 **Natice SQL 쿼리를 사용하거나 myBatis 같은 별도 Mapper를 사용해서 조회 기능을 구현**해야 한다.


---

# Agent Instructions: Querying This Documentation

If you need additional information that is not directly available in this page, you can query the documentation dynamically by asking a question.

Perform an HTTP GET request on the current page URL with the `ask` query parameter:

```
GET https://jihunparkme.gitbook.io/docs/book/ddd-start/05.md?ask=<question>
```

The question should be specific, self-contained, and written in natural language.
The response will contain a direct answer to the question and relevant excerpts and sources from the documentation.

Use this mechanism when the answer is not explicitly present in the current page, you need clarification or additional context, or you want to retrieve related documentation sections.
