1. 무슨 문제가 있었는지?
Chunk 기반의 스프링 배치를 개발하고 있는데 JpaPagingItemReader를 사용했을 때 PageSize를 설정해도 limit과 offset이 설정되지 않는 문제가 발생해서 원인을 분석중이다. 지난 번 포스팅에서는 ItemReader 의 동작 과정을 따라가던 중 설정했었던 LIMIT 정보가 사라지는 것을 확인하였다. 이번 포스팅에서는 그 원인을 찾아보려고 한다.
https://lazy-man.tistory.com/86
[Today I Learned - 13] JpaPagingItemReader Limit, Offset not working - (1)
1. 무슨 문제가 있었는지? 스프링 배치를 이용하여 Chunk 기반으로 개발하고 있었는데 ItemReader를 구현하는 부분에서 Limit, Offset이 적용되지 않아 전체 테이블을 조회하는 문제가 발생했는데 ItemRead
lazy-man.tistory.com
https://lazy-man.tistory.com/87
[Today I Learned - 14] JpaPagingItemReader Limit, Offset not working - (2)
1. 무슨 문제가 있었는지? Chunk 기반의 스프링 배치를 개발하고 있는데 JpaPagingItemReader를 사용했을 때 PageSize를 설정해도 limit과 offset이 설정되지 않는 문제가 발생해서 원인을 분석중이다. 지난
lazy-man.tistory.com
2. 문제의 원인은?
JpaPagingItemReader를 생성할 때 PaseSize 값을 설정하면 Limit 값이 저절로 생성될텐데 왜 비어있는 것일까? 문제의 원인은 DomainQueryExecutionContext 객체의 QueryOptions 구현체에 있다. 이전 포스팅 중 "★★ 기억 ★★" 주석을 달아놨던 executionContextFordoList 메서드를 살펴보자.
containsCollectionsFetches 변수와 hasLimit 변수를 판단하여 DomainQueryExecutionContext 객체를 설정하고 있다. Collection Fetch와 Limit을 사용하는 경우 omitSqlQueryOptions... 메서드를 통해 새로운 QueryOptions을 생성하고 그렇지 않은 경우 기존 설정값을 그대로 사용하도록 구현되어 있다.
protected DomainQueryExecutionContext executionContextFordoList(boolean containsCollectionsFetches,
boolean hasLimit, boolean needsDinstinct) {
final DomainQueryExecutionContext executionContextToUse;
if(hasLimit && containsCollectionFetches) {
// 생략...
final MutableQueryOptions originalQueryOptions = getQueryOptions(); // (1번 ★)
final QueryOptions normalizedQueryOptions;
// (2번 ★)
normalrizedQueryOptions = omitSqlQueryOptions...(originalQueryOptions, omitLimit:true, ...);
executionContextToUse = new DelegatingDomainQueryExecutionContext(this) {
@Override
public QueryOptions getQueryOptions() { return normailizedQueryOptions; }
}
}
else {
// 생략..
executionContextToUSE = this;
}
return executionContextToUse;
}
그렇다면 (1)번 위치와 (2)번 위치의 QueryOptions을 디버깅 모드로 살펴보면 (1)번 위치의 구현체는 QueryOptionsImpl 클래스이고 (2)번 위치의 구현체는 SqlOmittingQueryOptions 클래스이다.
아래 이미지에서 각각의 Limit 정보를 한번 찾아보자. 음.. 둘다 limit 정보는 0부터 20까지로 잘 설정되어 있는 것으로 보인다. 근데 왜 SqlAstTranslator 에서 limit 정보를 조회했을 때는 firstRow와 maxRows가 null 이었을까? 그 해답은 SqlOmittingQueryOptions 의 getLimit 메서드 구현 부분에 있었다.
(참고로 현재 SqlAstTranslator 에서 사용하는 QueryOptions의 구현체는 SqlOmittingQueryOptions이다)
다음은 SqlOmmitingQueryOptions의 getLimit 구현 부분이다. 위 코드에서 SqlOmmitingQueryOptions 객체를 생성할 때 omitLimit 값은 true로 설정되기 때문에 getLimit 메서드의 리턴값은 Limit.NONE 객체가 될 것이기 때문에 firstRow와 maxRows가 null 값으로 설정되는 것이였다.
// SqlOmmitingQueryOptions.class
@Override
public Limit getLimit() {return omitLimit? Limit.NONE : super.getLimit();}
// Limit.class
public class Limit {
public static final Limit NONE = new Limit();
private Integer firstRow;
private Intefer maxRows;
}
이와 같이 3개의 포스팅을 통해 JpaPagingItemReader 에서 limit 정보가 설정되지 않는 원인에 대해서 파악해보았다. 1줄로 요약하자면 limit과 fetch(일대다?)를 함께 사용하면 Limit 객체가 Limit.NONE 으로 설정되어 문제의 원인이 되었다는 것 !!
3. 해결방법
위와 같은 과정을 통해 limit과 fetch list를 같이 사용하면 QueryOptions 가 SqlOmittingQueryOptions 로 생성되면서 limit 정보가 Limit.NONE 객체를 사용하게 되면서 페이징 처리가 안되는 것을 알 수 있었다. 이와 같은 문제를 해결하기 위해 Query를 아래와 같이 FETCH 부분을 제거하였다. ItemProcessor와 ItemWriter 부근에서 item에 대한 참조가 없기 때문에 fetch 부분을 제거한 것이다.
// BEFORE
@Bean
public ItemReader<TaxInvoiceReserveEntity> exportItemReader() {
HashMap<String, Object> parameters = new HashMap<>();
parameters.put("param1", {{param1}});
parameters.put("param2", {{param2}});
return new JpaPagingItemReaderBuilder<TestEntity>()
.entityManagerFactory(entityManagerFactory)
.name("JpaPagingItemReader")
.pageSize(CHUNK_SIZE)
.queryString("""
SELECT tiReserve
FROM TestEntity t1
LEFT JOIN FETCH t1.items
WHERE t1.param1 =:param1
AND t1.param2 = :param2
ORDER BY t1.id ASC
""")
.parameterValues(parameters)
.build();
}
// AFTER
@Bean
public ItemReader<TaxInvoiceReserveEntity> exportItemReader() {
HashMap<String, Object> parameters = new HashMap<>();
parameters.put("param1", {{param1}});
parameters.put("param2", {{param2}});
return new JpaPagingItemReaderBuilder<TestEntity>()
.entityManagerFactory(entityManagerFactory)
.name("JpaPagingItemReader")
.pageSize(CHUNK_SIZE)
.queryString("""
SELECT tiReserve
FROM TestEntity t1
WHERE t1.param1 =:param1
AND t1.param2 = :param2
ORDER BY t1.id ASC
""")
.parameterValues(parameters)
.build();
}
4. 배운점
- QueryOptions 구현체 설정 조건 중 LIMIT과 FETCH ITEM을 함께 사용하면 SqlOmittingQueryOptions 을 구현체로 가지게된다.
- SqlOmittingQueryOptions 의 getLimit 메서드는 Limit.NONE 을 리턴한다.
5. 출처
'TIL(Today I Learned)' 카테고리의 다른 글
[Today I Learned - 17] ArrayList 의 sort() 오류 분석 (0) | 2024.05.20 |
---|---|
[Today I Learned - 16] JpaAuditing 적용으로 인한 단위 테스트 실패 (0) | 2024.02.23 |
[Today I Learned - 14] JpaPagingItemReader Limit, Offset not working - (2) (0) | 2024.02.01 |
[Today I Learned - 13] JpaPagingItemReader Limit, Offset not working - (1) (0) | 2024.01.31 |
[Today I Learned - 12] Spring Batch의 Tasklet Transaction (0) | 2024.01.25 |