728x90
JPA(native query)에서 MariaDB WITH RECURSIVE 작동 안되는 오류
게시글 상세조회에서 댓글의 대댓글까지 가져오는 쿼리를 JPA로 작성해야 했다. 일단 DB console에서 쿼리를 작성하고 정상 작동되는 걸 확인하고, native query로 옮겨와서 실행시켰는데 아예 실행조차 안되고 Bean 등록조차 안 되는 에러가 발생했다.
원인 코드
- 아래는 WITH RECURSIVE 사용한 일반 SQL 쿼리문
WITH RECURSIVE COMMENT_LEVEL AS (
SELECT
paper_comment_sn, comments, class, odr, parent_sn, reg_id, reg_dt,
concat(convert(paper_comment_sn, char), '-', '0') AS level -- 최상위 댓글은 0
FROM
PAPER_COMMENT
WHERE
parent_sn IS NULL -- 가장 상위 댓글을 선택
and del_yn = 'N'
and paper_sn = 1
UNION ALL
SELECT
pc.paper_comment_sn, pc.comments, pc.class, pc.odr, pc.parent_sn, pc.reg_id, pc.reg_dt,
concat(convert(pc.parent_sn, char), '-', convert(pc.odr, char)) AS level -- 부모 댓글 순번 + odr
FROM
PAPER_COMMENT pc
INNER JOIN
COMMENT_LEVEL cl ON pc.parent_sn = cl.paper_comment_sn
where pc.del_yn = 'N'
and pc.paper_sn = 1
)
SELECT
paper_comment_sn, comments, class, odr, parent_sn, reg_id, reg_dt, level
FROM COMMENT_LEVEL
order by level, odr;
# order by SUBSTRING_INDEX(level, '-', 1) desc, SUBSTRING_INDEX(level, '-', 2) asc;
- 아래는 위 쿼리문을 Native Query문으로 수정한 코드
@Query(
value = "WITH RECURSIVE COMMENT_LEVEL AS ( " +
" SELECT " +
" paper_comment_sn, comments, class, odr, parent_sn, reg_id, reg_dt, " +
" CONCAT(CAST(paper_comment_sn AS CHAR), '-', '0') AS level " +
" FROM " +
" PAPER_COMMENT " +
" WHERE " +
" parent_sn IS NULL " +
" AND del_yn = 'N' " +
" AND paper_sn = :paperSn " +
" UNION ALL " +
" SELECT " +
" pc.paper_comment_sn, pc.comments, pc.class, pc.odr, pc.parent_sn, pc.reg_id, pc.reg_dt, " +
" CONCAT(CAST(pc.parent_sn AS CHAR), '-', CAST(pc.odr AS CHAR)) AS level " +
" FROM " +
" PAPER_COMMENT pc " +
" JOIN " +
" COMMENT_LEVEL cl ON pc.parent_sn = cl.paper_comment_sn " +
" WHERE pc.del_yn = 'N' " +
" AND pc.paper_sn = :paperSn " +
") " +
"SELECT " +
" paper_comment_sn, comments, reg_id, reg_dt, level " +
"FROM COMMENT_LEVEL " +
"ORDER BY level, odr ",
nativeQuery = true
)
List<PaperCommentListDto> findPaperCommentListByPaperSn(@Param("paperSn") Integer paperSn);
- 위 내용처럼 repository에 작성하고 프로젝트를 실행하면 실행조차 안되고 Bean 등록도 안되는 에러가 발생했다.
- 아래는 에러 코드 내용이다.
org.springframework.beans.factory.BeanCreationException: Error creating bean with name 'requestMappingHandlerAdapter' defined in class path resource [org/springframework/boot/autoconfigure/web/servlet/WebMvcAutoConfiguration$EnableWebMvcConfiguration.class]: Bean instantiation via factory method failed; nested exception is org.springframework.beans.BeanInstantiationException: Failed to instantiate [org.springframework.web.servlet.mvc.method.annotation.RequestMappingHandlerAdapter]: Factory method 'requestMappingHandlerAdapter' threw exception; nested exception is org.springframework.beans.factory.BeanCreationException: Error creating bean with name 'querydslPredicateArgumentResolver' defined in class path resource [org/springframework/data/web/config/QuerydslWebConfiguration.class]: Bean instantiation via factory method failed; nested exception is org.springframework.beans.BeanInstantiationException: Failed to instantiate [org.springframework.data.web.querydsl.QuerydslPredicateArgumentResolver]: Factory method 'querydslPredicateArgumentResolver' threw exception; nested exception is org.springframework.beans.factory.BeanCreationException: Error creating bean with name 'querydslBindingsFactory' defined in class path resource [org/springframework/data/web/config/QuerydslWebConfiguration.class]: Initialization of bean failed; nested exception is org.springframework.beans.factory.BeanCreationException: Error creating bean with name 'paperCommentRepository' defined in repository.PaperCommentRepository defined in @EnableJpaRepositories declared on JpaRepositoriesRegistrar.EnableJpaRepositoriesConfiguration: Invocation of init method failed; nested exception is org.springframework.data.repository.query.QueryCreationException: Could not create query for public abstract java.util.List repository.PaperCommentRepository.findPaperCommentListByPaperSn(java.lang.Integer); Reason: null; nested exception is java.lang.NullPointerException
해결 1
가장 기본적으로 쿼리 select 문의 순서와 리턴 객체의 생성자 순서도 일치시키고,
alias도 추가해서 리턴 객체 필드와 일치시켜도 해결되지 않았다.
- 그 뒤로 WITH RECURSIVE 쿼리문을 일반 쿼리문으로 수정 후 다시 시도했다.
- (아래는 일반 쿼리문으로 수정한 SQL문)
SELECT
paper_comment_sn, comments, reg_id, reg_dt,
CONCAT(CAST(paper_comment_sn AS CHAR), '-', '0') AS level
FROM
PAPER_COMMENT
WHERE
parent_sn IS NULL
AND del_yn = 'N'
AND paper_sn = :paperSn
UNION ALL
SELECT
pc.paper_comment_sn, pc.comments, pc.reg_id, pc.reg_dt,
CONCAT(CAST(pc.parent_sn AS CHAR), '-', CAST(pc.odr AS CHAR)) AS level
FROM
PAPER_COMMENT pc
JOIN
PAPER_COMMENT pcp ON pc.parent_sn = pcp.paper_comment_sn
WHERE
pc.del_yn = 'N'
AND pc.paper_sn = :paperSn
ORDER BY level;
- (아래는 JPA Native Query 적용한 모습)
@Query(
value = "SELECT " +
" paper_comment_sn as paperCommentSn, " +
" comments, " +
" reg_id as regId, " +
" reg_dt as regDt, " +
" CONCAT(CAST(paper_comment_sn AS CHAR), '-', '0') AS level " +
"FROM " +
" PAPER_COMMENT " +
"WHERE " +
" parent_sn IS NULL " +
" AND del_yn = 'N' " +
" AND paper_sn = :paperSn " +
"UNION ALL " +
"SELECT " +
" pc.paper_comment_sn, pc.comments, pc.reg_id, pc.reg_dt, " +
" CONCAT(CAST(pc.parent_sn AS CHAR), '-', CAST(pc.odr AS CHAR)) AS level " +
"FROM " +
" PAPER_COMMENT pc " +
"JOIN " +
" PAPER_COMMENT pcp ON pc.parent_sn = pcp.paper_comment_sn " +
"WHERE " +
" pc.del_yn = 'N' " +
" AND pc.paper_sn = :paperSn " +
"ORDER BY level ",
nativeQuery = true
)
List<PaperCommentListDto> findPaperCommentListByPaperSn(@Param("paperSn") Integer paperSn);
💡 여기서 중요한 점은
- Spring Data JPA의 native query에서는 결과 컬럼의 이름을 DTO의 필드 이름과 정확히 일치시켜야 한다.
- 그리고 snake_case에서 camelCase로의 자동 변환은 native query에서 지원하지 않는다.
- 위처럼 작성하고 프로젝트 실행하면 실행은 되는데 메서드 호출 시 Tuple을 리턴 DTO로 맵핑 못 시키는 에러가 발생했다.
해결 2
- 리턴 객체를 repository에서 Tuple로 받고 service class에서 맵핑 작업을 추가로 해주는 방법을 시도했다.
1. service class에 javax.persistence.Tuple 객체를 import 하고
2. 맵핑 작업을 추가해 줬다.
List<Tuple> commentTupleList = paperCommentRepository.findPaperCommentListByPaperSn(paperSn);
viewDto.setCommentList(
commentTupleList.stream()
.map(tuple -> new PaperCommentListDto(
tuple.get("paperCommentSn", Integer.class),
tuple.get("comments", String.class),
tuple.get("regId", String.class),
tuple.get("regDt", LocalDateTime.class),
tuple.get("level", String.class)
))
.collect(Collectors.toList())
);
- 이렇게 수정하고 요청 오면 Cannot cast java.sql.Timestamp to java.time.LocalDateTime 에러가 발생했다.
해결 3
원인
- PaperCommonListDto 객체에서 regDt 필드를 LocalDateTime으로 선언했다.
- 그런데 JPA가 내부적으로 java.sql.Timestamp를 사용하여 DateTime 필드를 처리하기 때문에 발생했다.
- 즉, 데이터베이스에서 DateTime 값을 가져올 때 JPA는 이 값을 java.sql.Timestamp로 자동으로 변환한다.
- 그런 다음 service 레이어에서 이 값을 java.time.LocalDateTime으로 직접 변환하려고 할 때 ClassCastException이 발생을 한 것이다.
해결
- Tuple에서 값을 가져올 때 java.sql.Timestamp를 java.time.LocalDateTime으로 명시적으로 변환했다.
- 즉, 메모리 영역에서 .stream.map을 통해서 해결했다.
List<Tuple> commentTupleList = paperCommentRepository.findPaperCommentListByPaperSn(paperSn);
viewDto.setCommentList(
commentTupleList.stream()
.map(tuple -> new PaperCommentListDto(
tuple.get("paperCommentSn", Integer.class),
tuple.get("comments", String.class),
tuple.get("regId", String.class),
((Timestamp) tuple.get("regDt")).toLocalDateTime(),
tuple.get("level", String.class)
))
.collect(Collectors.toList())
);
마무리
이번 오류를 만나면서 굉장히 많은 것을 배웠다. native query에서도 WITH RECURSIVE 함수가 적용 안된다는것도 알았고, 스네이크 케이스에서 카멜 케이스로 자동 변환이 안된다는 것도 알았고, JPA가 내부적으로 Timestamp를 사용해서 DateTime 필드를 처리한다는 것도 알았다.
또한 Tuple 사용법도 이번 기회에 잘 알게 되었다.
해결하는데 오래 걸리긴 했지만 그래도 많은 걸 알고 가게 되어서 좋은 기회였다고 생각한다,,
JPQL에서 limit 1 작성했을때 실행 안되는 오류
'Spring > Spring Data JPA' 카테고리의 다른 글
JPA와 Spring Data JPA의 차이 (1) | 2024.01.02 |
---|---|
JPA N+1 문제 해결하기 (fetch join, entityGraph, batch size) (5) | 2023.12.28 |
[Spring Data JPA] ResponseDTO에 기본 생성자 있어야하는 이유 (1) | 2023.11.07 |
native query에서 @rank := 0, @rownum 처럼 사용자 정의 변수 사용할때 에러 발생 (1) | 2023.10.26 |
JPQL에서 limit 1 작성했을때 실행 안되는 오류 (1) | 2023.10.26 |