[QueryDsl] 스칼라 서브쿼리 (Select 서브쿼리) 에서 limit 처럼 사용하는 방법

최대한·2022년 3월 13일
0

도입

Domain <> DomainHistory 의 관계가 1:N 일 때
최신 이력을 가져오고 싶은 경우 보통 시간순으로 역정렬한 다음, limit 1 을 줘서 가져오는 방법을 생각할 수 있다.

본론

Prerequisite

  • Scala 서브쿼리란?
    • 서브쿼리의 한 종류로, Select 절에 사용되는 서브쿼리를 뜻한다.
    • 서브쿼리의 결과는 1개만 가능하다. (2개 이상일 경우 에러)

1. Orders.java

@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;
    }
}

2. OrdersHistory.java

@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;
    }
}

3. LatestOrdersDto.java

@ToString
public class LatestOrderDto {

    private Orders orders;

    private OrdersHistory ordersHistory;

    public LatestOrderDto(Orders orders, OrdersHistory ordersHistory) {
        this.orders = orders;
        this.ordersHistory = ordersHistory;
    }

}

4. OrdersRepositoryImpl.java

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();
    }
}

5. OrdersService.java

@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);
    }

}

6. OrdersRepositoryTest.java

@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개 이상 나온다고 한다. 쿼리를 보자

limit(1) Query

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 subquery

해결 방안을 고민하던 중, 문득 where 절에 가장 id 가 큰 놈을 반환하게끔 하면 되지 않을까 라는 생각이 들어
적용해보기로 했다.

OrdersRepositoryImpl.java

	
    ...


	@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();
    }

OrdersRepositoryTest.java

	...
    
    @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))

Query

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 절에서 한번 더 서브쿼리를 적용해줌으로써 문제를 해결할 수 있었다.

하지만 서브쿼리가 중첩으로 들어가기 때문에 쿼리가 복잡해짐을 알 수 있다.

혹여나 다른 해결방안이 있다면 공유해주길 바랍니다.

profile
Awesome Dev!

2개의 댓글

comment-user-thumbnail
2022년 9월 2일

안녕하세요! 비슷한 이슈를 경험하고 있어 댓글 남깁니다. 혹시 개선책 찾으셨나요??

답글 달기
comment-user-thumbnail
2023년 2월 7일

아 ㅋㅋ limit 왜 안먹누 ㅅㅂ

답글 달기