JPA 특정 컬럼 조회 feat.ConverterNot FoundException
[Springboot] JPA를 활용해 원하는 컬럼만 조회 해보자!
-
JPA로 요청에 알맞는 쿼리를 만들 때 정의한 엔티티 테이블의 일부 컬럼만 반환한다던가 집계함수, 조인 등을 사용해 다양한 결과를 출력해야 할 일이 있습니다.
-
간단한 서비스를 만들면서 실제로 겪고 공부하여 해결한 것을 바탕으로 쉬운 예시를 통해 기록해보고자 합니다.
요청 예시) 랭크 구간 별 유저 수를 조회하여 랭크 이름과 해당 랭크의 유저 수 현황을 뷰에 그래프로 보여주고자 한다.
Users 테이블
USER_NAME | USER_ID | USER_LEVEL | HIGH_RANK |
(PK)VARCHAR | VARCHAR | INTEGER | (FK)INTEGER |
Rank 테이블
DIVISION_ID | DIVISION_NAME |
(PK)INTEGER | VARCHAR |
두 테이블은 1:N 관계이고 요구사항을 처리하는 쿼리는 다음과 같습니다.
SELECT R.DIVISION_NAME AS DIVISION , COUNT(U.USER_ID) AS CNT
FROM USERS U
JOIN RANK R
ON U.HIGH_RANK = R.DIVISION_ID
GROUP BY R.DIVISION_NAME
ORDER BY 1 DESC;
-
결과 모양이 달라 기존에 정의된 Users 인스턴스에는 결과를 로드 할 수 없습니다.
-
Object[] 타입으로 반환하게 하면 결과에 액세스 할 수는 있지만 지저분하고 오류 발생이 쉬운 코드가 됩니다. - ref
While we can still access the results in the general-purpose Object[] returned in the list, doing so will result in messy, error-prone code.
에러 상황
적절한 Alias도 주었겠다. WrapperClass를 따로 정의 해 결과를 얻어보기로 하였습니다.
UserCntDivisionVo
@AllArgsConstructor
@NoArgsConstructor
@Getter
@Setter
public class UserCntDivisionVo {
private String division;
private Long cnt;
}
UserRepository
public interface UserRepository extends JpaRepository<User, String> {
@Query(value="select r.divisionName as division, count(u.userId) as cnt " +
"from Users u join Rank r on u.rank.divisionId = r.divisionId " +
"group by r.divisionName " +
"order by 1 desc ")
List<UserCntDivisionVo> findUserNumByDivisionError();
}
조회 결과 ConverterNotFoundException 예외가 발생합니다.
- JPA 에서 쿼리 결과에 대해 반환한 타입이 제가 정의한 클래스 타입으로 변환되지 못한 것입니다.
org.springframework.core.convert.ConverterNotFoundException:
No converter found capable of converting from type [org.springframework.data.jpa.repository.query.AbstractJpaQuery$TupleConverter$TupleBackedMap] to type [com.xyz.ff.UserCntDivisionVo]
해결하기
1. Custom Class With JPQL
- JPQL(Java Persistence Query Language) 란?
JPA 의 일부로 정의된 플랫폼 독립적 객체 지향 쿼리이다.
DB의 테이블이 아닌 JPA 엔티티에 대해 동작한다.(쿼리에 테이블 컬럼이름이 아니라 엔티티에서 표현되는 컬럼 이름을 사용해야함.)
- 객체 지향 방식으로 결과를 사용자가 정의할 수 있습니다.
SELECT 절의 컬럼을 POJO에 바인딩하는데 지정된 클래스에는 프로젝션 된 속성과 정확히 일치하는 생성자가 있어야 합니다.
UserRepository
public interface UserRepository extends JpaRepository<User, String> {
@Query(value = "SELECT new com.xyz.ff.UserCntDivisionVo(r.divisionName,count(u.userId)) " +
"FROM Users u " +
"Join Rank r ON u.rank.divisionId = r.divisionId " +
"GROUP BY r.divisionName " +
"ORDER BY 1 DESC")
List<UserCntDivisionVo> findUserNumByDivisionJpql();
}
2. Spring JPA Projection
- 인터페이스 기반 프로젝션을 통해 정의한 인터페이스를 기준으로 Repository 반환 값에 매핑해줍니다.
UserCntDivisionInterface
- 프로젝션 될 컬럼이름과 일치하게 인터페이스에 getter 메소드를 추가해야 합니다.
public interface UserCntDivisionInterface {
String getDivision();
Long getCnt();
}
UserRepository
public interface UserRepository extends JpaRepository<User, String> {
@Query(value="select r.divisionName as division, count(u.userId) as cnt " +
"from Users u join Rank r on u.rank.divisionId = r.divisionId " +
"group by r.divisionName " +
"order by 1 desc ")
List<UserCntDivisionInterface> findUserNumByDivisionInterface();
}
결과 테스트
TestCustomProjection
@ActiveProfiles("test")
@SpringBootTest
public class TestCustomProjection {
static List<UserCntDivisionInterface> listInterface;
static List<UserCntDivisionVo> listJpql;
@Autowired
private UserRepository userRepository;
@Test
@DisplayName("[에러 상황 - 에러 시 통과]")
void errorCustomTest() {
Exception e = assertThrows(Exception.class, () ->
userRepository.findUserNumByDivisionError());
System.out.println("에러메시지 : " + e.getMessage());
}
@Test
@DisplayName("[JPQL]")
void jpqlCustomTest() {
assertDoesNotThrow(() ->
userRepository.findUserNumByDivisionJpql());
System.out.println("JPQL 정상");
listJpql = userRepository.findUserNumByDivisionJpql();
assertNotNull(listJpql);
}
@Test
@DisplayName("[Spring JPA Projection]")
void interfaceCustomTest() {
assertDoesNotThrow(() ->
userRepository.findUserNumByDivisionInterface());
System.out.println("JPA 프로젝션 정상");
listInterface = userRepository.findUserNumByDivisionInterface();
assertNotNull(listInterface);
}
@AfterAll
public static void testBoth() {
assertEquals(listJpql.size(), listInterface.size());
for (int i = 0; i < listJpql.size(); i++) {
assertEquals(listJpql.get(i).getCnt(), listInterface.get(i).getCnt());
System.out.println(listJpql.get(i).getDivision()+" " + listJpql.get(i).getCnt());
}
}
}
결과
Reference