JPA template 이슈
템플릿 이슈
제목과 그리고 최상단의 주제인 템플릿 이슈 사항에 대해서 정리한다.
먼저 업무에서 생겼던 이슈사항부터 소개한다.
회사에서 postgreSQL을 사용한다.
그게 중요한 것은 아닌데, 각자의 db마다 방언이 있기 나름이고
테스트에서는 h2DB로 테스트를 하면서 방언도 h2방언을 설정해주었다.
로직중에 특정 date가 현재 시간보다 작은 경우를 쿼리로 찾아야했었는데,
to_date()
함수를 어떻게 적을것인가에 대한 문제였다.
왜 이 문제가 발생했냐면,
일단 기본적으로 여러개의 조건중에 저 하나가 들어가있었고, 문자열로 된 시간형식인 yyyyMMddHHmm
형식을 가지고 비교를
했어야 했기 때문이다.
이슈 파헤치기
일단 queryDsl
에서는 서브쿼리나 랜덤 등등 어떤 특정 조건을 만족해야 하는 식들같은 경우엔
하나같이 끝에 Expressions
가 붙었다.
대표적인건 뭐 JPAExpressions
이다.
위 사진은 JPAExpressions
의 일부이다.
이런식으로 다 함수들이 들어있을줄 알고 date 함수도 저런곳에 있겠지 하고 실행을 시키려고 했는데
존재하지가 않았다.
검색해본 결과 또 Template
류가 나오게 되었는데
DateTemplate
라는 곳에서 date관련 기능들을 사용할 수
있다고 해서 찾아보게 되었다.
DateTemplate
를 만드려면
Expressions
의 dateTemplate()
메소드를 사용해서 만들 수 있었다.
메소드의 인자들이다.
그래서 나도 똑같이 넣어주기 위해서
DateTemplate date = Expressions.dateTemplate(
LocalDateTime.class, "template???",
genesisReservation.reservationEndDt,
Expressions.constant("yyyyMMddHHmm")
)
이렇게 코드를 작성해주었다.
근데 작성하고 보니까 저기값은 String의 값인데
어떻게 넣어줘야하는지 감이 잡히질 않았다.
그래서 뒤지게된 JpaQueryFactory
JpaQueryFactory
의 내부중 일부를 가져왔다.
JPAQueryFactory
는 생성자에 별도 템플릿을 주지 않으면
JPAProvider
를 통한 JpQLTemplate를 가져온다.
여기보면 비슷하게 생긴 JPQLTemplates
가 있다.
저길 타고 들어가보니 또 Templates
를 상속받고 있었다.
JPQLTemplate에는 이제 익숙한 부분 쿼리문들이 메소드로 쭉 이어져 있었는데
그중의 일부를 보면
이렇게 생긴것들이 쭉 있다.
그래서 함수를 가져다가 저 조건에 맞게 쓰면 되는구나!
하고 유레카를 외쳤다. 굉장히 오래걸렸었다...😅
다시 돌아와서 템플릿 자리에 이렇게 바꿔주었다.
DateTemplate date = Expressions.dateTemplate(
LocalDateTime.class, "to_date({0}, {1})",
genesisReservation.reservationEndDt,
Expressions.constant("yyyyMMddHHmm")
)
따옴표를 써서 형식을 넣어줄거다 라고하면
to_date({0}, {1})
이렇게 해주면 잘되지만,
to_date({0}, '{1}')
이러면 형식을 맞춰주지 않으면 에러가 발생한다.
to_date({0}, '{1s}')
이럴경우엔 또 잘 실행이 되는데
string을 넣을 경우에 s를 붙이고 따옴표로 완성시키면 되는것인가 하면서 아직도 궁금한데 이부분은 해결하지 못했다.
마무리
이제 해결했으니까 본론으로 돌아와 querydsl을 다시 작성한다면,
@RequiredArgsConstructor
public class Test {
private final JPAQueryFactory JpaQueryFactory;
public void test() {
DateTemplate toDate = Expressions.dateTemplate(
LocalDateTime.class, "to_date({0}, {1})",
genesisReservation.reservationEndDt,
Expressions.constant("yyyyMMddHHmm")
);
return jpaQueryFactory.selectFrom(table)
.join(joinTable)
.on("조인조건")
.where("조건".and(toDate.lt(LocalDateTime.now())))
.fetch();
}
}
이런식으로 잘 들어오게 된다.
값도 테스트에서 비교하면 가정법 검증이 잘된다.
사이드 프로젝트에서의 이슈 사항
비슷하지만 다른 이슈였다.
사이드 프로젝트에서는 현재 MariaDB를 사용한다.
여기서는 다른 db와 다르게 rand() 를 사용한다.
postgreSQL, h2DB는 random()을 사용하는데
이것도 마찬가지로 템플릿에 대한 이슈였다.
JPAQueryFactory
에서 조금 다른걸 설명하고 싶은데
일단 조회쿼리는 JPAQuery<>
를,
수정쿼리는 JPAUpdateClause
, 삽입 JPAInsertClause
,
삭제 JPADeleteClause
를 사용한다.
그래서 나는 조회쿼리를 사용해야 했기 때문에 JPAQuery
클래스
안쪽을 찾아보았다.
자 이렇게 보면 아까 위에서 설명했던 JPQLTemplates
가 기본값으로
설정되어있기 때문에 그 템플릿을 사용하여 실행해서
그 끝쪽에 있는 Templates
에는
이미지 처럼 들어있었다.
그러니까 NumberExpressions의 random이 먹통이었던 것이다.
아무리 방언을 따른다고 해도 템플릿에 저장되어 있는것은
그대로 가는것 같다.
생각한 결론은,
그러면 bean으로 등록하면 되지 않을까?
였다.
그래서 바로 QuerydslConfiguration 설정을 진행해주었다.
일단 우리는 Hibernate
를 사용해서
기본값은 HQLTemplates
였고, 그래서
상속받아서 구현해주었다.
import com.querydsl.core.types.Ops;
import com.querydsl.jpa.HQLTemplates;
public class MariaDBTemplates extends HQLTemplates {
public static final MariaDBTemplates DEFAULT = new MariaDBTemplates();
protected MariaDBTemplates() {
this(DEFAULT_ESCAPE);
add(Ops.MathOps.RANDOM, "rand()");
add(Ops.MathOps.RANDOM2, "rand({0})");
}
public MariaDBTemplates(char escape) {
super(escape);
}
}
이렇게 구현해주고 나니 이제 빈을 등록해주면 되었다.
@Configuration
public class QuerydslConfig {
@PersistenceContext
private EntityManager entityManager;
@Bean
public JPAQueryFactory jpaQueryFactory() {
return new JPAQueryFactory(entityManager);
}
@Bean
public JPAQuery<NewBook> jpaQuery() {
return new JPAQuery<>(entityManager, MariaDBTemplates.DEFAULT);
}
}
이렇게 새책 테이블에 관한 random값을 사용할 수 있을것 같았고,
바로 querydsl을 짜러 갔다.
생성자 주입으로 먼저private final JPAQuery<NewBook> jpaQuery;
를 받아준 뒤에 로직은 그대로 두면 되겠거니 해서
그대로 두었다.
정상적으로 실행된다!!!!
컴퓨터를 옮기고 작업에 추가를 해줘야겠다.
위대로 설정하면 안된다. 상황 설명해주겠다.
현재 프로젝트는 위 설정처럼 진행했다.
그럴때 나오는 로그가 신박하다.
정리했을 처음처럼 random은 한번은 잘 출력되는데 문제는 그 이후부터이다.
이런식으로 새로고침을 할 때마다 random이 계속 호출된다.
그러니까 이 where
절과 order by
만 N + 1이 되는 것이다.
이게 일회성인지는 모르겠지만, 싱글톤 빈 주입으로 진행을 해주게 되면
계속 저걸로 영구적으로 바뀌어서 그런지 계속 추가가 된다.
처음은 몇줄 안되서 이렇겠지만 심각한 문제를 발생시킬것이다.
그 해결책은, 일단 빈주입이 아니라 쓰는 부분에만 특별하게 new를 해줘서 커스텀 해주는것이다.
@Override
public List<NewBook> selectRandom(int limit) {
JPAQuery<NewBook> jpaQuery = new JPAQuery<>(entityManager, MariaDBTemplates.DEFAULT);
return jpaQuery.from(newBook)
.orderBy(NumberExpression.random().asc())
.limit(limit)
.fetch();
}
이런식으로 사용처에만 할당해주니까 정상적으로 rand()
를 한번만 출력해주게 되었다.
결론
이제 조금씩 문서도 찾고, 라이브러리를 직접 뒤져가면서 학습해보니 결국 원초적인 지식이 가장 중요했다.
더 좋은 방법이 있다면 댓글로 남겨주셔도 좋습니다. 🙏
이렇게 보면 또 마이바티스처럼 되는 경향이 있다.
좀 더 데이터를 더럽고 간단하게 정제하고 자바쪽에서 부하를 걸어주는 방식을 계속 생각해야한다.
아무튼 오늘도 성장했다.