JPA

JPA template 이슈

리승자이 2022. 8. 7. 11:48
728x90

템플릿 이슈

제목과 그리고 최상단의 주제인 템플릿 이슈 사항에 대해서 정리한다.

먼저 업무에서 생겼던 이슈사항부터 소개한다.

회사에서 postgreSQL을 사용한다.

그게 중요한 것은 아닌데, 각자의 db마다 방언이 있기 나름이고

테스트에서는 h2DB로 테스트를 하면서 방언도 h2방언을 설정해주었다.

로직중에 특정 date가 현재 시간보다 작은 경우를 쿼리로 찾아야했었는데,

to_date() 함수를 어떻게 적을것인가에 대한 문제였다.

왜 이 문제가 발생했냐면,

일단 기본적으로 여러개의 조건중에 저 하나가 들어가있었고, 문자열로 된 시간형식인 yyyyMMddHHmm 형식을 가지고 비교를

했어야 했기 때문이다.

이슈 파헤치기

일단 queryDsl에서는 서브쿼리나 랜덤 등등 어떤 특정 조건을 만족해야 하는 식들같은 경우엔

하나같이 끝에 Expressions가 붙었다.

대표적인건 뭐 JPAExpressions이다.

image

위 사진은 JPAExpressions의 일부이다.

이런식으로 다 함수들이 들어있을줄 알고 date 함수도 저런곳에 있겠지 하고 실행을 시키려고 했는데

존재하지가 않았다.

검색해본 결과 또 Template 류가 나오게 되었는데

DateTemplate 라는 곳에서 date관련 기능들을 사용할 수

있다고 해서 찾아보게 되었다.

image

DateTemplate 를 만드려면

ExpressionsdateTemplate() 메소드를 사용해서 만들 수 있었다.

image

메소드의 인자들이다.

그래서 나도 똑같이 넣어주기 위해서

DateTemplate date = Expressions.dateTemplate(
    LocalDateTime.class, "template???",
            genesisReservation.reservationEndDt,
            Expressions.constant("yyyyMMddHHmm")
)

이렇게 코드를 작성해주었다.

근데 작성하고 보니까 저기값은 String의 값인데

어떻게 넣어줘야하는지 감이 잡히질 않았다.

그래서 뒤지게된 JpaQueryFactory

image

JpaQueryFactory 의 내부중 일부를 가져왔다.

JPAQueryFactory는 생성자에 별도 템플릿을 주지 않으면

JPAProvider를 통한 JpQLTemplate를 가져온다.

여기보면 비슷하게 생긴 JPQLTemplates가 있다.

저길 타고 들어가보니 또 Templates를 상속받고 있었다.

JPQLTemplate에는 이제 익숙한 부분 쿼리문들이 메소드로 쭉 이어져 있었는데

그중의 일부를 보면

image

이렇게 생긴것들이 쭉 있다.

그래서 함수를 가져다가 저 조건에 맞게 쓰면 되는구나!

하고 유레카를 외쳤다. 굉장히 오래걸렸었다...😅

다시 돌아와서 템플릿 자리에 이렇게 바꿔주었다.

DateTemplate date = Expressions.dateTemplate(
    LocalDateTime.class, "to_date({0}, {1})",
            genesisReservation.reservationEndDt,
            Expressions.constant("yyyyMMddHHmm")
)

따옴표를 써서 형식을 넣어줄거다 라고하면

to_date({0}, {1})

이렇게 해주면 잘되지만,

to_date({0}, '{1}')

이러면 형식을 맞춰주지 않으면 에러가 발생한다.

image

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 클래스
안쪽을 찾아보았다.

image

자 이렇게 보면 아까 위에서 설명했던 JPQLTemplates가 기본값으로

설정되어있기 때문에 그 템플릿을 사용하여 실행해서

그 끝쪽에 있는 Templates에는

image

이미지 처럼 들어있었다.

그러니까 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은 한번은 잘 출력되는데 문제는 그 이후부터이다.

스크린샷 2021-09-28 오후 11 54 47

이런식으로 새로고침을 할 때마다 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()를 한번만 출력해주게 되었다.

결론

이제 조금씩 문서도 찾고, 라이브러리를 직접 뒤져가면서 학습해보니 결국 원초적인 지식이 가장 중요했다.

더 좋은 방법이 있다면 댓글로 남겨주셔도 좋습니다. 🙏

이렇게 보면 또 마이바티스처럼 되는 경향이 있다.

좀 더 데이터를 더럽고 간단하게 정제하고 자바쪽에서 부하를 걸어주는 방식을 계속 생각해야한다.

아무튼 오늘도 성장했다.

728x90