Domain
<> DomainHistory
의 관계가 1:N
일 때
최신 이력을 가져오고 싶은 경우 보통 시간순으로 역정렬한 다음, limit 1 을 줘서 가져오는 방법을 생각할 수 있다.
@ToString
@Getter
@Entity
public class Orders {
@Id
@GeneratedValue(strategy = GenerationType.AUTO)
private Long id;
@Column
private String name;
@Column
@Enumerated(EnumType.STRING)
private OrderStatus orderStatus;
public static Orders from(String name){
Orders orders = new Orders();
orders.name = name;
orders.orderStatus = OrderStatus.READY;
return orders;
}
public void changeStatus(OrderStatus orderStatus) {
this.orderStatus = orderStatus;
}
}
@ToString
@Getter
@Entity
public class OrdersHistory {
@Id
@GeneratedValue(strategy = GenerationType.AUTO)
private Long id;
@ManyToOne(fetch = FetchType.LAZY)
@JoinColumn(name = "order_id")
@ToString.Exclude
private Orders orders;
@Column
@Enumerated(EnumType.STRING)
private OrderStatus orderStatus;
@Column
private LocalDateTime createdAt;
public OrdersHistory setOrders(Orders orders) {
this.orders = orders;
return this;
}
public static OrdersHistory of(Orders orders, OrderStatus orderStatus, LocalDateTime createdAt) {
OrdersHistory ordersHistory = new OrdersHistory();
ordersHistory.orders = orders;
ordersHistory.orderStatus = orderStatus;
ordersHistory.createdAt = createdAt;
return ordersHistory;
}
}
@ToString
public class LatestOrderDto {
private Orders orders;
private OrdersHistory ordersHistory;
public LatestOrderDto(Orders orders, OrdersHistory ordersHistory) {
this.orders = orders;
this.ordersHistory = ordersHistory;
}
}
public class OrdersRepositoryImpl extends QuerydslRepositorySupport implements OrdersCustomRepository {
public OrdersRepositoryImpl() {
super(Orders.class);
}
@Override
public LatestOrderDto findLatestOrderHistoryByLimit(Long orderId) {
return from(orders)
.leftJoin(ordersHistory)
.on(ordersHistory.orders.eq(orders))
.where(orders.id.eq(orderId))
.select(
Projections.constructor(LatestOrderDto.class,
orders,
Expressions.as(
from(ordersHistory)
.orderBy(ordersHistory.createdAt.desc())
.limit(1)
, "ordersHistory")
)
)
.fetchFirst();
}
}
@Service
@Transactional
@RequiredArgsConstructor
public class OrdersService {
private final OrdersRepository ordersRepository;
private final OrdersHistoryRepository ordersHistoryRepository;
public Orders save(Orders orders){
return ordersRepository.save(orders);
}
public void changeOrderStatus(Orders orders, OrderStatus orderStatus, LocalDateTime localDateTime){
orders.changeStatus(orderStatus);
OrdersHistory ordersHistory = OrdersHistory.of(orders, orderStatus, localDateTime);
ordersHistoryRepository.save(ordersHistory);
}
public LatestOrderDto findLatestOrderHistoryByLimit(Long orderId){
return ordersRepository.findLatestOrderHistoryByLimit(orderId);
}
}
@SpringBootTest
@Transactional
class OrdersRepositoryTest {
@Autowired
private OrdersService ordersService;
@Test
public void scalaSubQueryLimitTest() {
Orders orders = Orders.from("맥북 프로");
ordersService.save(orders);
ordersService.changeOrderStatus(orders, OrderStatus.READY, LocalDateTime.of(2022, 3, 5, 9, 0, 0));
ordersService.changeOrderStatus(orders, OrderStatus.SHIPPING, LocalDateTime.of(2022, 3, 6, 9, 0, 0));
ordersService.changeOrderStatus(orders, OrderStatus.DELIVERED, LocalDateTime.of(2022, 3, 7, 9, 0, 0));
LatestOrderDto latestOrderHistoryOpt = ordersService.findLatestOrderHistoryByLimit(orders.getId());
}
}
Caused by: org.h2.jdbc.JdbcSQLDataException:
Scalar subquery contains more than one row
; SQL statement:
select orders0.id as col_0_0, (select ordershist2.id from orders_history ordershist2) as col1_0, orders0.id as id1_2, orders0.name as name2_2, orders0.order_status as order_st3_2 from orders orders0 left outer join orders_history ordershist1 on (ordershist1.order_id=orders0.id) where orders0_.id=? limit ? [90053-200] ...
쿼리를 실행하니 분명 limit(1) 을 해줬음에도 불구하고 결과가 1개 이상 나온다고 한다. 쿼리를 보자
select
orders0_.id as col_0_0_,
(select
ordershist2_.id
from
orders_history ordershist2_
order by
ordershist2_.created_at desc) as col_1_0_,
orders0_.id as id1_2_,
orders0_.name as name2_2_,
orders0_.order_status as order_st3_2_
from
orders orders0_
inner join
orders_history ordershist1_
on (
ordershist1_.order_id=orders0_.id
)
where
orders0_.id=? limit ?
여기에서 서브쿼리부분을 보면
(select
ordershist2_.id
from
orders_history ordershist2_
order by
ordershist2_.created_at desc) as col_1_0_,
어떤 이유에서인지는 모르겠지만 limit 절이 누락되어있다.
해당 이유는 다음에 정확히 알아보도록 하고
우선 해결 방안부터 알아보자.
해결 방안을 고민하던 중, 문득 where 절에 가장 id 가 큰 놈을 반환하게끔 하면 되지 않을까 라는 생각이 들어
적용해보기로 했다.
...
@Override
public LatestOrderDto findLatestOrderHistoryByWhereSubQuery(Long orderId) {
return from(orders)
.innerJoin(ordersHistory)
.on(ordersHistory.orders.eq(orders))
.where(orders.id.eq(orderId))
.select(
Projections.constructor(LatestOrderDto.class,
orders,
Expressions.as(
from(ordersHistory)
.where(
ordersHistory.id.eq(
JPAExpressions
.select(ordersHistory.id.max())
.from(ordersHistory)
.where(ordersHistory.orders.id.eq(orderId))
)
)
, "ordersHistory")
)
)
.fetchFirst();
}
...
@Test
public void scalaSubQueryLimitTest() {
Orders orders = Orders.from("맥북 프로");
ordersService.save(orders);
ordersService.changeOrderStatus(orders, OrderStatus.READY, LocalDateTime.of(2022, 3, 5, 9, 0, 0));
ordersService.changeOrderStatus(orders, OrderStatus.SHIPPING, LocalDateTime.of(2022, 3, 6, 9, 0, 0));
ordersService.changeOrderStatus(orders, OrderStatus.DELIVERED, LocalDateTime.of(2022, 3, 7, 9, 0, 0));
Exception e = Assertions.assertThrows(Exception.class, () -> {
ordersService.findLatestOrderHistoryByLimit(orders.getId());
});
System.out.println("Exception message: " + e.getMessage());
LatestOrderDto latestOrderHistoryByWhereSubQuery = ordersService.findLatestOrderHistoryByWhereSubQuery(orders.getId());
System.out.println(latestOrderHistoryByWhereSubQuery);
}
LatestOrderDto(orders=Orders(id=1, name=맥북 프로, orderStatus=DELIVERED), ordersHistory=OrdersHistory(id=4, orderStatus=DELIVERED, createdAt=2022-03-07T09:00))
select
orders0_.id as col_0_0_,
(select
ordershist2_.id
from
orders_history ordershist2_
where
ordershist2_.id=(
select
max(ordershist3_.id)
from
orders_history ordershist3_
where
ordershist3_.order_id=?
)
) as col_1_0_, orders0_.id as id1_2_, orders0_.name as name2_2_, orders0_.order_status as order_st3_2_
from
orders orders0_
inner join
orders_history ordershist1_
on (
ordershist1_.order_id=orders0_.id
)
where
orders0_.id=? limit ?
서브쿼리 부분을 다시 보면
(select
ordershist2_.id
from
orders_history ordershist2_
where
ordershist2_.id =
(select
max(ordershist3_.id)
from
orders_history ordershist3_
where
ordershist3_.order_id=?)
) as col_1_0_, orders0_.id as id1_2_, orders0_.name as name2_2_, orders0_.order_status as order_st3_2_
의도한 대로 해당 주문건의 history 중 id 가 가장 높은 (가장 최신의) history row 를 select 했음을 확인할 수 있다.
위에서 살펴본 대로 JPA 에서 Scala subQuery 를 이용할 경우 limit 절이 정상적으로 작동하지 않는 버그가 있어서
where 절에서 한번 더 서브쿼리를 적용해줌으로써 문제를 해결할 수 있었다.
하지만 서브쿼리가 중첩으로 들어가기 때문에 쿼리가 복잡해짐을 알 수 있다.
혹여나 다른 해결방안이 있다면 공유해주길 바랍니다.
안녕하세요! 비슷한 이슈를 경험하고 있어 댓글 남깁니다. 혹시 개선책 찾으셨나요??