/////
Search
😵‍💫

QueryDSL 로 알맞은 데이터 뽑아내기

태그
QueryDSL
JPA
목차

QueryDsl 과 JPQL(JPA Query Language)

QueryDSLJPQL(JPA Query Language)은 모두 Java에서 데이터베이스 질의를 위한 언어입니다.

QueryDSL

QueryDSL은 SQL, JPA, JDO, Lucene, Collection 등 여러 저장소를 위한 유형 안전(type-safe) 쿼리 언어입니다.
프로그래밍 방식으로 복잡한 쿼리를 구성하고, 컴파일 시간에 타입 안전성을 검사할 수 있게 해주는 프레임워크입니다.
장점
타입 안전성: 컴파일 시점에서 오류를 잡을 수 있어 런타임 오류의 가능성을 줄입니다.
동적 쿼리 지원: 프로그래밍 방식으로 쿼리를 작성할 수 있으므로, 런타임에 동적으로 쿼리를 생성하고 변경할 수 있습니다.
코드 기반 구성: 자바 코드를 사용하여 쿼리를 작성하므로, IDE의 자동 완성, 리팩토링, 문법 강조 등의 이점을 활용할 수 있습니다.
다양한 저장소 지원: 다양한 저장소에 대한 쿼리를 동일한 방식으로 작성할 수 있어 학습 곡선을 줄입니다.

JPQL (Java Persistence Query Language)

JPQL은 JPA를 사용하여 데이터베이스에 질의하기 위한 언어입니다.
SQL과 유사하지만, 데이터베이스 테이블 대신 엔티티 객체에 대해 작동합니다.
엔티티 중심의 쿼리: 데이터베이스 테이블이 아닌 엔티티 객체와 그 필드에 대해 쿼리를 작성합니다.
표준화: JPQL은 JPA의 일부로, Java EE 표준을 따릅니다.
런타임 쿼리 평가: JPQL 쿼리는 런타임에 해석되며, 이는 컴파일 시점에서의 타입 안전성이 부족함을 의미합니다.
QueryDSL은 복잡하고 동적인 쿼리를 쉽게 작성하고 관리할 수 있게 해주는 반면, JPQL은 JPA 표준의 일부로서 엔티티 중심의 쿼리를 가능하게 합니다.

요구 사항을 들여다 보면

왜 queryDSL 을 썼나?

단순히 queryDSL 을 써보고 싶어서도 이유가 될 수 있겠지만 모든 기술을 과하게 적용하는건 지양해야합니다. 간단한 쿼리만 필요한데 굳이 쓸 필요는 없다는 뜻이죠. 하지만 이번 과제의 요구사항을 보니 써야겠다 라는 생각이 들었습니다.
과제의 요구사항은 아래와 같습니다.
쿼리 파라미터를 이용하여 요청 게시글을 반환합니다.
query
속성
default(미입력 시 값)
설명
hashtag
string
본인계정
type
string (열거형)
필수 값
date, hour
start
date
오늘로 부터 7일전
2023-10-01 과 같이 데이트 형식이며 조회 기준 시작일을 의미합니다.
end
date
오늘
2023-10-25 과 같이 데이트 형식이며 조회 기준 시작일을 의미합니다.
value
string
count
count , view_count, like_count, share_count 가 사용 가능합니다.
?value=count&type=date 
start ~ end 기간내 (시작일, 종료일 포함) 해당 hashtag 가 포함된 게시물 수를 일자별로 제공합니다.
최대 한달(30일) 조회 가능합니다.
?value=count&type=hour 
start ~ end 기간내 (시작일, 종료일 포함) 해당 hashtag 가 포함된 게시물 수를 시간별로 제공합니다.
start 일자의 00시 부터 1시간 간격
최대 일주일(7일) 조회 가능합니다.
type 으로 들어갈 수 있는 속성은 date, hour 이 있고, value 로 들어갈수 있는 속성은 count, view_count, like_count, share_count 가 있습니다. 때문에 조회하고자 할 때 조건을 동적으로 조합하여 최종 쿼리를 생성해야 합니다.
 동적쿼리란?
동적 쿼리(dynamic query)는 실행 시간에 쿼리의 구조가 결정되는 쿼리를 의미합니다. 때문에 쿼리의 일부 또는 전체가 런타임에 조건에 따라 변경될 수 있습니다.
그래서 쿼리 DSL 을 사용했고, 목표하는 결과는 아래와 같았습니다.
// date + counter 중 하나 선택시 { "date": "날짜", "value": 54 } // hour + counter 중 하나 선택시 // date + counter 중 하나 선택시 { "날짜" [ { "시간": 시간, "value": 50 }, { ... }, ... ] }
Java
복사

Type = Date 일 때

왜 값이 이상하게 나오지?

처음에는 아래와 같이 쿼리를 작성했습니다.
@Transactional public List<ResolverResponse> findPostsByTypeDateAndValue (StatisticRequest request) { NumberExpression<Long> dynamicField = findDynamicField(request); List<ResolverResponse> posts = new JPAQueryFactory(entityManager) .select(Projections.constructor(ResolverResponse.class, post.createdAt, dynamicField) ).from(post) .innerJoin(post.hashTags, hashTag1) .where(hashTag1.hashTag.eq(request.getHashTag()) .and(post.createdAt.between(request.getStartDate(), request.getEndDate()))) .groupBy(post.createdAt) .orderBy(post.createdAt.desc()) .fetch(); return posts; }
Java
복사
하지만 예상과 다르게 나오는 결과…
[ { "createdAt": "2023-11-17T16:03:59.790537", "dynamicValue": 1 }, { "createdAt": "2023-11-17T16:03:59.391601", "dynamicValue": 1 }, { "createdAt": "2023-11-17T16:03:59.048236", "dynamicValue": 1 }, { "createdAt": "2023-11-17T16:03:58.702", "dynamicValue": 1 }, { "createdAt": "2023-11-17T16:03:58.372926", "dynamicValue": 1 }, ... ]
Java
복사
어쩌면 당연했습니다. 받은값은 LocalDateTime 값인데 .groupBy(post.createdAt) 을 했으니 시간까지 포함해서 묶여버린 거겠죠…
그렇게 날짜만 추출하기 위한 여정이 시작되었습니다.

날짜만 어떻게 뽑을까?

처음에는 LocalDate 타입을 생각했습니다. 그래서 아래와 같이 작성했죠.
@Transactional public List<ResolverResponse> findPostsByTypeDateAndValue (StatisticRequest request) { DateTimeTemplate<LocalDate> dateOnly = Expressions.dateTimeTemplate(LocalDate.class, "FUNCTION('DATE', {0})", post.createdAt); List<ResolverResponse> posts = new JPAQueryFactory(entityManager) .select(Projections.constructor(ResolverResponse.class, dateOnly, dynamicField) ) .from(post) .innerJoin(post.hashTags, hashTag1) .where(hashTag1.hashTag.eq(request.getHashTag()) .and(post.createdAt.between(request.getStartDate(), request.getEndDate()))) .groupBy(dateOnly) .orderBy(dateOnly.desc()) .fetch(); return posts; }
Java
복사
하지만 마주한 에러
java.lang.IllegalArgumentException: argument type mismatch at java.base/jdk.internal.reflect.NativeConstructorAccessorImpl.newInstance0(Native Method) ~[na:na] at java.base/jdk.internal.reflect.NativeConstructorAccessorImpl.newInstance(NativeConstructorAccessorImpl.java:77) ~[na:na] at java.base/jdk.internal.reflect.DelegatingConstructorAccessorImpl.newInstance(DelegatingConstructorAccessorImpl.java:45) ~[na:na]
Plain Text
복사
리스폰스를 주입할때에는 타입과 순서가 같아야 한다고한다. 하지만 나의 경우는 알맞았기 때문에 원인은 이게 아니였다.
아니면 다른곳에서 메서드를 잘못쓴건가? 했지만 그런것도 아니였다. 정말 맞왜틀의 늪에 빠져버렸다.
그러다가 발견한 문장.
MySQL에서 날짜 부분만 추출하는 DATE() 함수의 반환 타입이 실제로 LocalDate와 호환되는지 확인하세요. 때로는 반환 타입이 Date이거나 다른 타입일 수 있습니다. 필요한 경우, 반환 타입을 적절히 변환하는 로직을 추가할 수 있습니다.
(아니 다 통일해주면 안되냐고여?)
타입만 바꿔주니 아래와 같이 원하는 값을 잘 뱉어내더군요!
[ { "dateOnly": "2023-11-17", "dynamicValue": 5 }, { "dateOnly": "2023-11-16", "dynamicValue": 60 }, { "dateOnly": "2023-11-15", "dynamicValue": 6 }, { "dateOnly": "2023-11-14", "dynamicValue": 5 } ]
JSON
복사

Type = hour 일 때

이놈은 더했다.

처음에 아래와 같이 작성했었는데,
public List<Object[]> findPostsByTypeHourAndValue (StatisticRequest request) { NumberExpression<Long> dynamicField = findDynamicField(request); String queryString = "SELECT FUNCTION('DATE', post.createdAt) as date, " + "FUNCTION('HOUR', post.createdAt) as hour, " + "COUNT(post) as countOfPost, " + dynamicField + " as dynamicCount " + "FROM Post post " + "JOIN post.user user " + "JOIN post.hashTags hashTag " + "WHERE post.createdAt BETWEEN :startDate AND :endDate " + "AND hashTag.hashTag = :hashTagName " + "GROUP BY date, hour "; return entityManager.createQuery(queryString, Object[].class) .setParameter("startDate", request.getStartDate()) .setParameter("endDate", request.getEndDate()) .setParameter("hashTagName", request.getHashTag()) .getResultList(); }
Java
복사
해당 게시글이 존재하지 않습니다.
Plain Text
복사
(앞에서 체력 다써서 힘없는 상태의 김시은)
그래도 힘을내서 다시 해보기로 했습니다. 왠지 FUNCTION('HOUR', post.createdAt) 이놈이 의심스러웠어요. 혹시 MySQL 은 또 다른건 아닐까? 했지만 찾아보니 MySQL 에는 HOUR 함수가 있습니다.
JPQL 에서는 FUNCTION() 을 활용하여 hibernate 에 등록된 각 DataBase의 Dialect 에 정의된 function 을 사용합니다. 문법에도 맞는데 왜 안될까 싶어서 FUNCTION() 을 쓰지않고 아래와 같이 수정해보았습니다.
@Transactional public List<Object[]> findPostsByDateAndHour(StatisticRequest request) { String jpql = "SELECT DATE(post.createdAt) as date, " + "HOUR(post.createdAt) as hour, " + "COUNT(post) as count " + "FROM Post post " + "JOIN post.hashTags hashTag " + "WHERE post.createdAt BETWEEN :startDate AND :endDate " + "AND hashTag.hashTag = :hashTagName " + "GROUP BY date, hour " + "ORDER BY date DESC, hour DESC"; return entityManager.createQuery(jpql, Object[].class) .setParameter("startDate", request.getStartDate()) .setParameter("endDate", request.getEndDate()) .setParameter("hashTagName", request.getHashTag()) .getResultList(); }
Java
복사
그리고 나온 결과
[ [ "2023-11-17", 16, 5 ], [ "2023-11-16", 16, 2 ], [ "2023-11-16", 15, 58 ], [ "2023-11-15", 15, 6 ], [ "2023-11-14", 15, 4 ], [ "2023-11-14", 14, 1 ] ]
JSON
복사
방언을 인식하지 못하는걸까요? 과제에 제한시간이 있어 자세히 파보지는 못했지만, 시간나면 다시 검토해봐야 겠다는 생각이 들었습니다.  일단 제대로된 값을 추출하는 것에 만족하기로 했습니다. (구현은 했으니까요?)