QLRM(Query Language Result Mapper)은 JPA와 함께 사용할 수 있는 라이브러리로, 네이티브 SQL 쿼리 결과를 엔티티가 아닌 특정 객체(특히 DTO)에 매핑할 때 유용합니다. JPA 자체로는 복잡한 쿼리나 특정 형식으로 결과를 반환하는 데 제약이 있을 수 있기 때문에, QLRM을 사용하면 이를 보완할 수 있습니다. 즉 따로 매핑을 안해도 된다.
1.문제상황
select id, title, (select count(id) from reply_tb where board_id = bt.id) count
from board_tb bt;
서브쿼리 문제
일단 jpql로는 select문에 서브쿼리를 사용하는 것은 어렵거나 불가능하다
결국 네이티브 쿼리로 만들어야한다.
(select count(id) from reply_tb where board_id = bt.id) count
- 이 부분은 서브쿼리를 사용하여
reply_tb
테이블에서board_tb
의 각 게시글에 연결된 댓글 수를 계산하는 것입니다.JPQL
에서는 서브쿼리를 사용할 수 있지만, 서브쿼리를SELECT
절에 직접 넣는 것은 지원하지 않습니다. 이는 JPA가 제공하는 객체 지향적인 쿼리 작성 방식에서는 제한이 있기 때문입니다.
2.2 다른 데이터베이스 연산 또는 집계 함수
또한, JPA는 SQL의 일부 고급 기능이나 특정 데이터베이스에서 지원하는 연산자, 집계 함수 등의 복잡한 사용을 완전히 지원하지 않을 수 있습니다. 이 경우 네이티브 SQL 쿼리를 작성하는 것이 더 효율적입니다.
2.QlRM을 사용하지않고 Object 수동매핑
package org.example.springv3.board;
import jakarta.persistence.EntityManager;
import jakarta.persistence.Query;
import lombok.RequiredArgsConstructor;
import org.springframework.stereotype.Repository;
import java.util.ArrayList;
import java.util.List;
@RequiredArgsConstructor
@Repository
public class BoardQueryRepository {
private final EntityManager em;
public List<BoardListVo> selectV1() {
// 네이티브 SQL 쿼리
String sql = """
select id, title, (select count(id) from reply_tb where board_id = bt.id) as count
from board_tb bt;
""";
Query query = em.createNativeQuery(sql);
// 쿼리 결과를 Object[] 리스트로 받음
List<Object[]> resultList = query.getResultList();
// Object[] 리스트를 BoardListVo DTO로 매핑
List<BoardListVo> boardListVoList = new ArrayList<>();
for (Object[] result : resultList) {
// 각 컬럼 값을 추출 (순서는 쿼리에서 반환되는 순서대로)
Long id = ((Number) result[0]).longValue(); // 첫 번째 컬럼: id
String title = (String) result[1]; // 두 번째 컬럼: title
Long count = ((Number) result[2]).longValue(); // 세 번째 컬럼: count
// DTO 생성 및 리스트에 추가
BoardListVo boardListVo = new BoardListVo(id, title, count);
boardListVoList.add(boardListVo);
}
return boardListVoList;
}
}
2.1설명
createNativeQuery(sql)
을 통해 네이티브 SQL 쿼리를 실행하고, 그 결과를Object[]
로 반환받습니다. 네이티브 쿼리는 항상 기본적으로 배열 형태로 데이터를 반환합니다.
- 쿼리 결과에서 각각의
Object[]
배열은 쿼리에서 반환한 각 행의 데이터를 포함합니다. 각 배열의 인덱스는 쿼리에서 선택한 컬럼의 순서를 따릅니다: result[0]
:id
(게시물 ID)result[1]
:title
(게시물 제목)result[2]
:count
(댓글 개수, 서브쿼리의 결과)
- 결과를 각 필드에 맞게 변환하여
BoardListVo
객체를 생성하고 리스트에 추가합니다.
2.2주의사항
result[0]
,result[2]
에서Long
타입으로 변환할 때는Number
타입을 먼저 받아야 합니다. 왜냐하면 SQL에서 반환되는 수치형 데이터는Number
타입이기 때문입니다.
- 타입 캐스팅 시 주의해야 하며, 필요할 경우 데이터 타입이 적절하게 변환되는지 확인해야 합니다.
이 방법은 수동으로 매핑해야 하므로 코드가 다소 번거롭지만, 네이티브 쿼리를 사용하면서 복잡한 데이터를 엔티티가 아닌 DTO로 처리해야 할 때는 이러한 방식이 필요할 수 있습니다.
3.QlRM사용
그래들 설정
implementation group: 'org.qlrm', name: 'qlrm', version: '4.0.1'
package org.example.springv3.board;
import jakarta.persistence.EntityManager;
import jakarta.persistence.Query;
import lombok.RequiredArgsConstructor;
import org.qlrm.mapper.JpaResultMapper;
import org.springframework.stereotype.Repository;
import java.util.List;
@RequiredArgsConstructor
@Repository
public class BoardQueryRepository {
private final EntityManager em;
public List<BoardResponse.ListDTO> selectV1(){
String sql = """
select id, title, (select count(id) from reply_tb where board_id = bt.id) count
from board_tb bt;
""";
Query query = em.createNativeQuery(sql);
JpaResultMapper mapper = new JpaResultMapper();
List<BoardResponse.ListDTO> boardList = mapper.list(query, BoardResponse.ListDTO.class);
return boardList;
}
}
요약
Repository 계층에서 DTO로 매핑을 끝내는 목적은 서비스 계층에서 객체를 별도로 매핑하는 번거로움을 줄이고, 코드 가독성과 유지보수를 개선하려는 것
Tip
매핑을 할때에 보통의 경우 dto를 만들어서 받고 이 떄까지 사용한 lazy전략을 이용한 지연로딩을 사용하는 방식은 entity객체를 사용하는게 아니라 reposity에서 dto를 받아 반환하는 걸 기억하자
Share article