
- 주문내역에서 추가로 주문한 상품 정보를 조회
Order기준으로 컬렉션인OrderItem과Item이 필요함
package jpabook.jpashop.domain;
...
@Entity
@Table(name = "orders")
@Getter @Setter
@NoArgsConstructor(access = AccessLevel.PROTECTED)
public class Order {
...
@OneToMany(mappedBy = "order", cascade = CascadeType.ALL)
private List<OrderItem> orderItems = new ArrayList<>();
...
}
package jpabook.jpashop.domain;
...
@Entity
@Table(name = "order_item")
@Getter @Setter
@NoArgsConstructor(access = AccessLevel.PROTECTED)
public class OrderItem {
...
@JsonIgnore
@ManyToOne(fetch = FetchType.LAZY)
@JoinColumn(name = "order_id")
private Order order;
...
}
OneToOne, ManyToOne) 관계만 있었고, 이번 섹션4에는 컬렉션인 일대다 관계(OneToMany)를 조회하고 최적화하는 방법에 대해 알아봄package jpabook.jpashop.api;
import jpabook.jpashop.domain.Order;
import jpabook.jpashop.domain.OrderItem;
import jpabook.jpashop.repository.OrderRepository;
import jpabook.jpashop.repository.OrderSearch;
import lombok.RequiredArgsConstructor;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.GetMapping;
import java.util.List;
@Controller
@RequiredArgsConstructor
public class OrderApiController {
private final OrderRepository orderRepository;
/**
* V1. 엔티티 직접 노출
* - 엔티티가 변하면 API 스펙이 변함
* - 트랜잭션 안에서 지연 로딩 필요
* - 양방향 연관관계 문제
* @return List<Order>
*/
@GetMapping("/api/v1/orders")
public List<Order> ordersV1() {
List<Order> all = orderRepository.findAll(new OrderSearch());
for(Order order : all) {
order.getMember().getName(); //Lazy 강제 초기화
order.getDelivery().getAddress(); //Lazy 강제 초기화
List<OrderItem> orderItems = order.getOrderItems();
orderItems.stream().forEach(o -> o.getItem().getName()); //Lazy 강제 초기화
}
return all;
}
}
OrderItem, item 관계를 직접 초기화하면 Hibernate5Module 설정에 의해 엔티티를 JSON으로 생성함
양방향 연관관계면 무한루프에 걸리지 않도록 한곳에 @JsonIgnore를 추가해야 함
OrderItem 엔티티에서 order에 @JsonIgnore이 추가되어 있음엔티티가 변하면 API 스펙이 변하고, 엔티티를 직접 노출하므로 좋은 방법은 아님

OrderItem까지 추가로 조회 완료public class OrderApiController {
...
/**
* V2. 엔티티를 조회해서 DTO로 변환
* - 트랜잭션 안에서 지연 로딩 필요
* @return List<OrderDto>
*/
@GetMapping("/api/v2/orders")
public List<OrderDto> orderV2() {
List<Order> orders = orderRepository.findAll(new OrderSearch());
List<OrderDto> result = orders.stream()
.map(o -> new OrderDto(o))
.collect(toList());
return result;
}
@Data
static class OrderDto {
private Long orderId;
private String name;
private LocalDateTime orderDate;
private OrderStatus orderStatus;
private Address address;
private List<OrderItem> orderItems;
public OrderDto(Order order) {
orderId = order.getId();
name = order.getMember().getName();
orderDate = order.getOrderDate();
orderStatus = order.getStatus();
address = order.getDelivery().getAddress();
order.getOrderItems().stream().forEach(o -> o.getItem().getName());
orderItems = order.getOrderItems();
}
}
}

위 코드 작성 후 v2 api도 실행을 해보면 v1과 동일한 결과가 출력되지만, 해당 방식으로 코드를 작성해서는 안됨
왜냐하면 OrderItem 엔티티가 그대로 외부에 노출이 되어버림
따라서 OrderItem도 DTO로 변환한 후에 반환하도록 수정해야 함
package jpabook.jpashop.api;
...
@RestController
@RequiredArgsConstructor
public class OrderApiController {
private final OrderRepository orderRepository;
...
/**
* V2. 엔티티를 조회해서 DTO로 변환
* - 트랜잭션 안에서 지연 로딩 필요
* @return List<OrderDto>
*/
@GetMapping("/api/v2/orders")
public List<OrderDto> orderV2() {
List<Order> orders = orderRepository.findAll(new OrderSearch());
List<OrderDto> result = orders.stream()
.map(o -> new OrderDto(o))
.collect(toList());
return result;
}
@Data
static class OrderDto {
private Long orderId;
private String name;
private LocalDateTime orderDate;
private OrderStatus orderStatus;
private Address address;
private List<OrderItemDto> orderItems;
public OrderDto(Order order) {
orderId = order.getId();
name = order.getMember().getName();
orderDate = order.getOrderDate();
orderStatus = order.getStatus();
address = order.getDelivery().getAddress();
orderItems = order.getOrderItems().stream()
.map(orderItem -> new OrderItemDto(orderItem))
.collect(toList());
}
}
@Data
static class OrderItemDto {
private String itemName;
private int orderPrice;
private int count;
public OrderItemDto(OrderItem orderItem) {
itemName = orderItem.getItem().getName();
orderPrice = orderItem.getOrderPrice();
count = orderItem.getCount();
}
}
}

OrderItem 출력 시, 원하는 필드만 출력이 가능함
하지만 지연 로딩으로 너무 많은 SQL이 실행됨
- SQL 실행 수
order1번member,addressN번(order 조회 수 만큼)orderItemN번(order 조회 수 만큼)itemN번(orderItem 조회 수 만큼)
지연로딩은 영속성 컨텍스트에 있으면서 영속성 컨텍스트에 있는 엔티티를 사용하고 없으면 SQL을 실행함
같은 영속성 컨텍스트에서 이미 로딩한 회원 엔티티를 추가로 조회하면 SQL을 실행하지 않음
따라서 위 방식을 활용할 수 있는 fetch join으로 성능 최적화가 필요함
OrderApiController
package jpabook.jpashop.api;
...
@RestController
@RequiredArgsConstructor
public class OrderApiController {
private final OrderRepository orderRepository;
...
/**
* V3. 엔티티를 조회해서 DTO로 변환
* - fetch join 최적화
* @return List<OrderDto>
*/
@GetMapping("/api/v3/orders")
public List<OrderDto> orderV3() {
List<Order> orders = orderRepository.findAllWithItem();
List<OrderDto> result = orders.stream()
.map(o -> new OrderDto(o))
.collect(toList());
return result;
}
@Data
static class OrderDto {
private Long orderId;
private String name;
private LocalDateTime orderDate;
private OrderStatus orderStatus;
private Address address;
private List<OrderItemDto> orderItems;
public OrderDto(Order order) {
orderId = order.getId();
name = order.getMember().getName();
orderDate = order.getOrderDate();
orderStatus = order.getStatus();
address = order.getDelivery().getAddress();
orderItems = order.getOrderItems().stream()
.map(orderItem -> new OrderItemDto(orderItem))
.collect(toList());
}
}
@Data
static class OrderItemDto {
private String itemName;
private int orderPrice;
private int count;
public OrderItemDto(OrderItem orderItem) {
itemName = orderItem.getItem().getName();
orderPrice = orderItem.getOrderPrice();
count = orderItem.getCount();
}
}
}
OrderRepository
public List<Order> findAllWithMemberDelivery() {
return em.createQuery(
"select o from Order o" +
" join fetch o.member m" +
" join fetch o.delivery d", Order.class)
.getResultList();
}
public List<Order> findAllWithItem() {
return em.createQuery(
"select o from Order o" +
" join fetch o.member m" +
" join fetch o.delivery d" +
" join fetch o.orderItems oi" +
" join fetch oi.item i", Order.class)
.getResultList();
}
member와 delivery 조인은 이전에 작성한 ToOne과 동일하게 작동이 됨orderItems와 item의 fetch join
ORDER와 ORDER_ITEM을 조인해보면 똑같은 ORDER_ID의 값이 2번 중복되어 조회되는 것을 확인할 수 있음ORDER_ID를 기준으로 조인을 수행함ORDER_ITEM 테이블에 1번 ORDER_ID가 다른 ROW에 2번 적재되어 있기 때문에
select
o1_0.order_id,
d1_0.delivery_id,
d1_0.city,
d1_0.street,
d1_0.zipcode,
d1_0.status,
m1_0.member_id,
m1_0.city,
m1_0.street,
m1_0.zipcode,
m1_0.name,
o1_0.order_date,
o2_0.order_id,
o2_0.order_item_id,
o2_0.count,
i1_0.item_id,
i1_0.dtype,
i1_0.name,
i1_0.price,
i1_0.stock_quantity,
i1_0.artist,
i1_0.etc,
i1_0.author,
i1_0.isbn,
i1_0.actor,
i1_0.director,
o2_0.order_price,
o1_0.status
from
orders o1_0
join
member m1_0
on m1_0.member_id=o1_0.member_id
join
delivery d1_0
on d1_0.delivery_id=o1_0.delivery_id
join
order_item o2_0
on o1_0.order_id=o2_0.order_id
join
item i1_0
on i1_0.item_id=o2_0.item_id

order 엔티티의 조회 수도 증가를 하게 됨 for(Order order : orders) {
System.out.println("order ref=" + order + " id=" + order.getId();
}

public List<Order> findAllWithItem() {
return em.createQuery(
"select distinct o from Order o" +
" join fetch o.member m" +
" join fetch o.delivery d" +
" join fetch o.orderItems oi" +
" join fetch oi.item i", Order.class)
.getResultList();
}
distinct 키워드를 사용하여 쿼리 작성

select
distinct o1_0.order_id,
d1_0.delivery_id,
d1_0.city,
d1_0.street,
d1_0.zipcode,
d1_0.status,
m1_0.member_id,
m1_0.city,
m1_0.street,
m1_0.zipcode,
m1_0.name,
o1_0.order_date,
o2_0.order_id,
o2_0.order_item_id,
o2_0.count,
i1_0.item_id,
i1_0.dtype,
i1_0.name,
i1_0.price,
i1_0.stock_quantity,
i1_0.artist,
i1_0.etc,
i1_0.author,
i1_0.isbn,
i1_0.actor,
i1_0.director,
o2_0.order_price,
o1_0.status
from
orders o1_0
join
member m1_0
on m1_0.member_id=o1_0.member_id
join
delivery d1_0
on d1_0.delivery_id=o1_0.delivery_id
join
order_item o2_0
on o1_0.order_id=o2_0.order_id
join
item i1_0
on i1_0.item_id=o2_0.item_id

distinct 키워드가 추가되었지만, 여전히 데이터는 중복되어 출력이 되고 있음
ORDER_ID에 대해서만 각각 값들이 출력되는 것을 볼 수 있음ORDER를 가지고 올 때, 쿼리에 distinct라는 키워드가 있는 상태에서 같은 ORDER_ID 값이 있으면 중복을 제거하여 출력함order가 컬렉션 페치 조인 때문에 중복 조회되는 것을 막아줌distinct를 추가entity가 중복으로 조회되는 경우, 애플리케이션에서 중복을 걸러서 컬렉션에 담아줌distinct과 다른 추가적인 기능 public List<Order> findAllWithItem() {
return em.createQuery(
"select distinct o from Order o" +
" join fetch o.member m" +
" join fetch o.delivery d" +
" join fetch o.orderItems oi" +
" join fetch oi.item i", Order.class)
.setFirstResult(1)
.setMaxResults(100)
.getResultList();
}

Out of memory 문제가 발생할 수 있음