단건 조회에서 많이 사용하는 방식
OrderApiController 코드 추가package jpabook.jpashop.api;
...
@RestController
@RequiredArgsConstructor
public class OrderApiController {
private final OrderQueryRepository orderQueryRepository;
/**
* V4. JPA에서 DTO 직접 조회
* @return List<OrderQueryDto>
*/
@GetMapping("/api/v4/orders")
public List<OrderQueryDto> ordersV4() {
return orderQueryRepository.findOrderQueryDtos();
}
}
OrderQueryRepository 새로 생성package jpabook.jpashop.repository.order.query;
...
@Repository
@RequiredArgsConstructor
public class OrderQueryRepository {
private final EntityManager em;
/**
* 컬렉션은 별도로 조회
* Query : 루트 1번, 컬렉션 N번
* 단건 조회에서 많이 사용하는 방식
* @return List<OrderQueryDto>
*/
public List<OrderQueryDto> findOrderQueryDtos() {
List<OrderQueryDto> result = findOrders();
result.forEach(o -> {
List<OrderItemQueryDto> orderItems = findOrderItems(o.getOrderId());
o.setOrderItems(orderItems);
});
return result;
}
/**
* 1 : N 관계(컬렉션)를 제외한 나머지를 한 번에 조회
* @return List<OrderQueryDto>
*/
public List<OrderQueryDto> findOrders() {
return em.createQuery(
"select new jpabook.jpashop.repository.order.query.OrderQueryDto(o.id, m.name, o.orderDate, o.status, d.address)" +
" from Order o" +
" join o.member m" +
" join o.delivery d", OrderQueryDto.class)
.getResultList();
}
/**
* 1 : N 관계인 OrderItems 조회
* @param orderId
* @return List<OrderItemQueryDto>
*/
private List<OrderItemQueryDto> findOrderItems(Long orderId) {
return em.createQuery(
"select new jpabook.jpashop.repository.order.query.OrderItemQueryDto(oi.order.id, i.name, oi.orderPrice, oi.count)" +
" from OrderItem oi" +
" join oi.item i" +
" where oi.order.id = : orderId", OrderItemQueryDto.class)
.setParameter("orderId", orderId)
.getResultList();
}
}
OrderQueryRepository에서 정의OrderRepository에 정의OrderQueryDtopackage jpabook.jpashop.repository.order.query;
...
@Data
public class OrderQueryDto {
private Long orderId;
private String name;
private LocalDateTime orderDate;
private OrderStatus orderStatus;
private Address address;
private List<OrderItemQueryDto> orderItems;
public OrderQueryDto(Long orderId, String name, LocalDateTime orderDate, OrderStatus orderStatus, Address address) {
this.orderId = orderId;
this.name = name;
this.orderDate = orderDate;
this.orderStatus = orderStatus;
this.address = address;
}
}
OrderItemQueryDtopackage jpabook.jpashop.repository.order.query;
...
@Data
public class OrderItemQueryDto {
private Long orderId;
private String itemName;
private int orderPrice;
private int count;
public OrderItemQueryDto(Long orderId, String itemName, int orderPrice, int count) {
this.orderId = orderId;
this.itemName = itemName;
this.orderPrice = orderPrice;
this.count = count;
}
}


findOrders 수행시 호출됨
두번째 쿼리 : findOrderItems 수행시 호출됨
ToOne 관계이기 때문에 서로 조인해도 무방함총 3번의 쿼리(1 + N번)가 수행됨
findOrders 쿼리 수행 1번으로 2개가 나왔고, findOrderItems 쿼리가 2번 수행됨N개의 결과가 나오면, 두번째 컬렉션 쿼리는 N번 수행됨
Query: 루트 1번, 컬렉션 N번 실행
ToOne(N:1, 1:1)관계들을 먼저 조회하고,ToMany(1:N)관계는 각각 별도로 처리함
- 이런 방식을 선택하는 이유
ToOne관계는 조인해도 데이터 row 수가 증가하지 않음ToMany관계는 조인하면 row 수가 증가함
- row 수가 증가하지 않는 ToOne 관계는 조인으로 최적화하기 쉬우므로 한번에 조회
- ToMany 관계는 최적화하기 어려우므로
findOrderItems()같은 별도의 메서드에서 조회
데이터를 한꺼번에 처리할 때 많이 사용하는 방식
OrderApiController에 추가 /**
* V5. JPA에서 DTO 직접 조회 - 컬렉션 조회 최적화
* @return List<OrderQueryDto>
*/
@GetMapping("/api/v5/orders")
public List<OrderQueryDto> ordersV5() {
return orderQueryRepository.findAllByDto_optimization();
}
OrderQueryRepository에 추가package jpabook.jpashop.repository.order.query;
...
@Repository
@RequiredArgsConstructor
public class OrderQueryRepository {
private final EntityManager em;
...
/**
* 최적화
* Query : 루트 1번, 컬렉션 1번
* 데이터를 한꺼번에 처리할 때 많이 사용하는 방식
* @return List<OrderQueryDto>
*/
public List<OrderQueryDto> findAllByDto_optimization() {
// 루트 조회(toOne 코드를 모두 한 번에 조회)
List<OrderQueryDto> result = findOrders();
// orderItem 컬렉션을 MAP 한방에 조회
List<Long> orderIds = result.stream()
.map(o -> o.getOrderId())
.collect(Collectors.toList());
List<OrderItemQueryDto> orderItems = em.createQuery(
"select new jpabook.jpashop.repository.order.query.OrderItemQueryDto(oi.order.id, i.name, oi.orderPrice, oi.count)" +
" from OrderItem oi" +
" join oi.item i" +
" where oi.order.id in :orderIds", OrderItemQueryDto.class)
.setParameter("orderIds", orderIds)
.getResultList();
Map<Long, List<OrderItemQueryDto>> orderItemMap = orderItems.stream()
.collect(Collectors.groupingBy(orderItemsQueryDto -> orderItemsQueryDto.getOrderId()));
// 루프를 돌면서 컬렉션 추가(추가 쿼리 실행X)
result.forEach(o -> o.setOrderItems(orderItemMap.get(o.getOrderId())));
return result;
}
...
}
package jpabook.jpashop.repository.order.query;
...
@Repository
@RequiredArgsConstructor
public class OrderQueryRepository {
private final EntityManager em;
...
/**
* 최적화
* Query : 루트 1번, 컬렉션 1번
* 데이터를 한꺼번에 처리할 때 많이 사용하는 방식
* @return List<OrderQueryDto>
*/
public List<OrderQueryDto> findAllByDto_optimization() {
// 루트 조회(toOne) 코드를 모두 한 번에 조회
List<OrderQueryDto> result = findOrders();
// orderItem 컬렉션을 MAP 한방에 조회
Map<Long, List<OrderItemQueryDto>> orderItemMap = _findOrderItemMap(_toOrderIds(result));
// 루프를 돌면서 컬렉션 추가(추가 쿼리 실행X)
result.forEach(o -> o.setOrderItems(orderItemMap.get(o.getOrderId())));
return result;
}
private List<Long> _toOrderIds(List<OrderQueryDto> result) {
return result.stream()
.map(o -> o.getOrderId())
.collect(Collectors.toList());
}
private Map<Long, List<OrderItemQueryDto>> _findOrderItemMap(List<Long> orderIds) {
List<OrderItemQueryDto> orderItems = em.createQuery(
"select new jpabook.jpashop.repository.order.query.OrderItemQueryDto(oi.order.id, i.name, oi.orderPrice, oi.count)" +
" from OrderItem oi" +
" join oi.item i" +
" where oi.order.id in :orderIds", OrderItemQueryDto.class)
.setParameter("orderIds", orderIds)
.getResultList();
return orderItems.stream()
.collect(Collectors.groupingBy(orderItemsQueryDto -> orderItemsQueryDto.getOrderId()));
}
}


첫번째 쿼리 findOrders 1번 수행
두번째 쿼리 findOrderItems 1번 수행
총 2번의 쿼리 수행
Query: 루트 1번, 컬렉션 1번
ToOne관계들을 먼저 조회하고, 여기서 얻은 식별자orderId로ToMany관계인OrderItem을 한꺼번에 조회
MAP을 사용해서 매칭 성능 향상(O(1))
return List<OrderFlatDto>Order와 OrderItem을 조인하고, OrderItem과 Item을 조인하여 한방쿼리를 만들어서 DB를 한방에 가져옴flat하게 SQL 조인의 결과를 가져올 수 있음OrderApiController
package jpabook.jpashop.api;
...
@RestController
@RequiredArgsConstructor
public class OrderApiController {
...
/**
* V6. JPA에서 DTO 직접 조회 - 플랫 데이터 최적화
* @return List<OrderQueryDto>
*/
@GetMapping("/api/v6/orders")
public List<OrderFlatDto> ordersV6() {
return orderQueryRepository.findAllByDto_flat();
}
}
OrderFlatDto
package jpabook.jpashop.repository.order.query;
...
@Data
public class OrderFlatDto {
private Long orderId;
private String name;
private LocalDateTime orderDate; //주문시간
private Address address;
private OrderStatus orderStatus;
private String itemName;//상품명
private int orderPrice; //주문가격
private int count; //주문수량
public OrderFlatDto(Long orderId, String name, LocalDateTime orderDate, OrderStatus orderStatus, Address address, String itemName, int orderPrice, int count) {
this.orderId = orderId;
this.name = name;
this.orderDate = orderDate;
this.orderStatus = orderStatus;
this.address = address;
this.itemName = itemName;
this.orderPrice = orderPrice;
this.count = count;
}
}
OrderQueryRepository
package jpabook.jpashop.repository.order.query;
...
@Repository
@RequiredArgsConstructor
public class OrderQueryRepository {
...
public List<OrderFlatDto> findAllByDto_flat() {
return em.createQuery(
"select new " +
"jpabook.jpashop.repository.order.query.OrderFlatDto(o.id, m.name, o.orderDate, o.status, d.address, i.name, oi.orderPrice, oi.count)" +
" from Order o" +
" join o.member m" +
" join o.delivery d" +
" join o.orderItems oi" +
" join oi.item i", OrderFlatDto.class)
.getResultList();
}
}

[
{
"orderId": 1,
"name": "userA",
"orderDate": "2024-04-28T22:04:34.667737",
"address": {
"city": "서울",
"street": "1",
"zipcode": "1111"
},
"orderStatus": "ORDER",
"itemName": "JPA1 BOOK",
"orderPrice": 10000,
"count": 1
},
{
"orderId": 1,
"name": "userA",
"orderDate": "2024-04-28T22:04:34.667737",
"address": {
"city": "서울",
"street": "1",
"zipcode": "1111"
},
"orderStatus": "ORDER",
"itemName": "JPA2 BOOK",
"orderPrice": 20000,
"count": 2
},
{
"orderId": 2,
"name": "userB",
"orderDate": "2024-04-28T22:04:34.815809",
"address": {
"city": "진주",
"street": "2",
"zipcode": "2222"
},
"orderStatus": "ORDER",
"itemName": "SPRING1 BOOK",
"orderPrice": 20000,
"count": 3
},
{
"orderId": 2,
"name": "userB",
"orderDate": "2024-04-28T22:04:34.815809",
"address": {
"city": "진주",
"street": "2",
"zipcode": "2222"
},
"orderStatus": "ORDER",
"itemName": "SPRING2 BOOK",
"orderPrice": 40000,
"count": 4
}
]

Query는 한 번 호출됨OrderItems가 기준이 되어서 중복된 값들이 출력됨return List<OrderQueryDto>OrderFlatDto를 반환하는 것이 아니라 OrderQueryDto 스펙에 맞춰야하는 경우, stream으로 중복된 값들을 제외하여 호출해주면 됨package jpabook.jpashop.api;
...
@RestController
@RequiredArgsConstructor
public class OrderApiController {
...
/**
* V6. JPA에서 DTO 직접 조회 - 플랫 데이터 최적화
* @return List<OrderQueryDto>
*/
@GetMapping("/api/v6/orders")
public List<OrderQueryDto> ordersV6() {
List<OrderFlatDto> flats = orderQueryRepository.findAllByDto_flat();
return flats.stream()
.collect(groupingBy(o -> new OrderQueryDto(o.getOrderId(), o.getName(), o.getOrderDate(), o.getOrderStatus(), o.getAddress()),
mapping(o -> new OrderItemQueryDto(o.getOrderId(), o.getItemName(), o.getOrderPrice(), o.getCount()), toList())
)).entrySet().stream()
.map(e -> new OrderQueryDto(e.getKey().getOrderId(),
e.getKey().getName(), e.getKey().getOrderDate(), e.getKey().getOrderStatus(),
e.getKey().getAddress(), e.getValue()))
.collect(toList());
}
}
package jpabook.jpashop.repository.order.query;
...
@Data
@EqualsAndHashCode(of = "orderId")
public class OrderQueryDto {
private Long orderId;
private String name;
private LocalDateTime orderDate;
private OrderStatus orderStatus;
private Address address;
private List<OrderItemQueryDto> orderItems;
public OrderQueryDto(Long orderId, String name, LocalDateTime orderDate, OrderStatus orderStatus, Address address) {
this.orderId = orderId;
this.name = name;
this.orderDate = orderDate;
this.orderStatus = orderStatus;
this.address = address;
}
public OrderQueryDto(Long orderId, String name, LocalDateTime orderDate,
OrderStatus orderStatus, Address address, List<OrderItemQueryDto> orderItems) {
this.orderId = orderId;
this.name = name;
this.orderDate = orderDate;
this.orderStatus = orderStatus;
this.address = address;
this.orderItems = orderItems;
}
}

[
{
"orderId": 1,
"name": "userA",
"orderDate": "2024-04-28T22:37:54.959234",
"orderStatus": "ORDER",
"address": {
"city": "서울",
"street": "1",
"zipcode": "1111"
},
"orderItems": [
{
"orderId": 1,
"itemName": "JPA1 BOOK",
"orderPrice": 10000,
"count": 1
},
{
"orderId": 1,
"itemName": "JPA2 BOOK",
"orderPrice": 20000,
"count": 2
}
]
},
{
"orderId": 2,
"name": "userB",
"orderDate": "2024-04-28T22:37:55.076539",
"orderStatus": "ORDER",
"address": {
"city": "진주",
"street": "2",
"zipcode": "2222"
},
"orderItems": [
{
"orderId": 2,
"itemName": "SPRING1 BOOK",
"orderPrice": 20000,
"count": 3
},
{
"orderId": 2,
"itemName": "SPRING2 BOOK",
"orderPrice": 40000,
"count": 4
}
]
}
]

API를 수행해보면 쿼리도 1번만 수행되고 OrderQueryDto 스펙에 맞춰서 데이터가 중복없이 호출됨쿼리는 한 번 수행되지만 조인으로 인해 DB에서 애플리케이션에 전달하는 데이터에 중복 데이터가 추가됨
따라서 상황에 따라 V5보다 더 느릴 수 있음
애플리케이션에서 추가 작업이 큼
페이징이 불가능함
OrderItems 기준으로는 페이징을 할 수는 있지만 Order 기준으로는 페이징이 불가능함