ToOne 관계와 일대다 관계를 따로 분리해서 조회한 것 입니다.
/**
* 컬렉션은 별도로 조회
* Query: 루트 1번, 컬렉션 N 번
* 단건 조회에서 많이 사용하는 방식
* OrderQueryRepository는 엔티티가 아닌 특정 화면들의 핏한 쿼리들을 위한 것 (관심사가 분리됨)
*/
public List<OrderQueryDto> findOrderQueryDtos() {
//루트 조회(toOne 코드를 모두 한번에 조회)
List<OrderQueryDto> result = findOrders();
//루프를 돌면서 컬렉션 추가(추가 쿼리 실행)
result.forEach(o -> {
List<OrderItemQueryDto> orderItems = findOrderItems(o.getOrderId());
o.setOrderItems(orderItems);
});
return result;
}
/**
* 1:N 관계(컬렉션)를 제외한 나머지를 한번에 조회
*/
private 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 조회
*/
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();
}
QueryDsl로 바꾸면 이런 방식으로 됨.
private List<OrderItemQueryDto> findOrderItems(Long orderId) {
JPAQueryFactory queryFactory = new JPAQueryFactory(em);
return queryFactory
.select(new QOrderItemQueryDto(
orderItem.order.id,
item.name,
orderItem.orderPrice,
orderItem.count))
.from(orderItem)
.join(orderItem.item, item)
.where(orderItem.order.id.eq(orderId))
.fetch();
}
ToOne 관계는 조인해도 데이터 row 수가 증가하지 않는데 ToMany 관계는 조인하면 row 수가 증가합니다. (데이터 뻥튀기)
ToMany는 루프를 돌면서 채워넣어 OrderItem을 가지고 오고 set해서 반환해준 것.
하지만 이렇게 할 경우 Order 1번 쿼리, OrderItem이 N번 쿼리가 나가 N+1 문제가 발생합니다.
컬렉션이 루프를 돌지 않고 한방에 가져오는 방법입니다.
/**
* 최적화
* Query: 루트 1번, 컬렉션 1번
* 데이터를 한꺼번에 처리할 때 많이 사용하는 방식
*
*/
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;
}
// OrderQueryDto인데 Long 형태로 바꾼 것 -> OrderId의 리스트로 만들기 위함
// findOrderItemMap에서 jpql에 in절 안에 파라미터 인자로 넣어주기 위함
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(OrderItemQueryDto::getOrderId));
}
기존에는 where절에 order.id를 =을 통해 하나씩 가져왔지만 in을 활용해 한꺼번에 가져와 버린 것입니다.
또한 groupingBy를 사용해서 OrderId를 기준으로 Map으로 변형이 가능합니다. 그러면 키가 OrderId, 값은 OrderItemQueryDto가 됩니다.
기존 것과 차이점
querydsl이라면?
List<OrderItemQueryDto> orderItems = queryFactory .select(new QOrderItemQueryDto(orderItem.order.id, order.name, orderItem.orderPrice, orderItem.count)) .from(orderItem) .join(orderItem.item, order) .where(orderItem.order.id.in(orderIds)) .fetch();
이번에는 쿼리 한방으로만 해결하는 방법입니다.
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();
}
querydsl로 수정
public List<OrderFlatDto> findAllByDto_flat() {
JPAQueryFactory queryFactory = new JPAQueryFactory(em);
return queryFactory
.select(new QOrderFlatDto(o.id, m.name, o.orderDate, o.status, d.address, i.name, oi.orderPrice, oi.count))
.from(o)
.join(o.member, m)
.join(o.delivery, d)
.join(o.orderItems, oi)
.join(oi.item, i)
.fetch();
}
@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());
}
하지만 조인으로 인해 DB에서 애플리케이션에 전달하는 데이터에 중복 데이터가 추가되므로 상황에 따라 V5보다 느릴 수 있습니다. -> 데이터 양이 적다면 이게 더 빠를 수 있습니다.
또한 페이징도 불가능합니다.