DB에 있는 데이터를 복잡한 쿼리로 조회하고 결과 데이터를 또 다시 필요한 필드만으로 DTO로 바꾸는 과정을 깔끔하게 하는 방법이 어떤게 있을까?
Database에 Sales에 대한 Table이 있다고 가정할 때
만들고자 하는 DTO는 다음과 같습니다.
(필드는 DB의 애트리뷰트의 자료형과 맞추어서 만들었습니다.)
@Getter
@NoArgs Constructor
@AllArgsConstructor
public class SalesCountDTO{
private long id;
private String name;
private Long sales;
}
...
@Transactional
public List<SalesCountDTO> getSalesByStream(){
return saleRepo.findAll()
.parallelStream()
.map(sale -> new SalesCountDTO(sale.getId(), sale.getName(), sale.getSales()))
.collect(
Collectors.toMap(
sum -> sum.getId(),
Function.identify(),
(sum1, sum2) -> new SaleSum(
sum1.getId(),
sum1.getSales.add(sum2.getSales())
)
)
)
.values()
.stream()
.sorted(Comparator.comparing(SalesCountDTO::getsales))
.collect(Collectors.toList())
}
collect
를 이용하면 다양한 groupBy
를 사용할 수 있습니다.
Collectors.toMap
을 이용하여 Sale.getId()
를 기준으로 SaleSum
을 생성하는 방식으로 사용하였습니다.
Stream
을 사용하게 되면 어플리케이션 단에서 구현된 내용을 쉽게 수정하거나 파악할 수 있는 장점이 있지만 전체 Data를 어플리케이션에서 group by
를 하기 때문에 데이터가 많아지면 느려지고 메모리 사용이 많아지기에 적절한 방식이 아닙니다.
JPA
에 JPQL
과 Projection
을 이용해서 DTO
에 Query
결과값을 담는다.
@Repository
public interface SalesRepository extends JpaRepository<Sales, Long>{
@Query(
"SELECT "
+ "new study.jpa.sales.dto.SalesCountDTO(s.Id, s.name, SUM(s.sale)) "
+ "FROM Sale s "
+ "GROUP BY s.ID"
)
List<SalesCountDTO> findSalesCountDtoJPQL();
}
JPQL
을 이용하면 Query
에 대한 타입 안정성과 Query
안정성을 바로 확인 할수 있지만 Projection
을 사용할 때 new 를 통해 생성자를 쿼리 내에서 호출해 줘야하는 단점이 있습니다.
Native Query
에 대해 Entity
가 아닌 Retury 값을 반환받기 위해서는 Interface based Projection
을 활용해야 합니다.
Interface
선언
public interface SalesCountInterface{
Long getId();
String getName();
Long getSales();
}
Interface
에 맞게 Native Query
를 작성
@Repository
public interface SalesRepository extends JpaRepository<Sales, Long>{
@Query(
"SELECT "
+ "s.Id AS id "
+ ", s.Name As name "
+ ", SUM(Sales) AS sales "
+ "FROM Sale s "
+ "GROUP BY s.ID"
, nativeQuery = true
)
List<SalesCountDTO> findSalesCountDtoNativeQuery();
}
JPQL
과 유사하지만 nativeQuery=true
를 통해 순수 Query
로 작성되어 있으며, SELECT된 각 칼럼을 Interface
명에 맞춰 Alias
를 설정해야 됩니다.
Native Query
를 활용하게 되면 쿼리 자체를 Database에서 빠르게 검증 가능하지만, 반환된 Interface
를 별도의 DTO로 변환해야 할 필요성이 생깁니다.
QueryDSL
을 활용한 Group By
쿼리는 JPAQuery
를 직접 활용해야 합니다.
@Service
@Slf4j
@RequiredArgsConstructor
public class SalesServiceImpl implements SalesService {
@PersistenceContext
private EntityManager entityManager;
@Transactional
public List<SalesCountDTO> getSalesDTOByQueryDSL(){
QSale qSale = QSale.sale;
JPAQueryFactory qf = new JPAQueryFactory(entityManager);
JPAQuery<SalesCountDTO> query = qf.from(qSale)
.groupBy(qSale.id)
.select(
Projections.bean(
SalesCountDTO,
qSale.id,
qSale.name,
qSale.sales.sum().as("sales")
)
);
return query.fetch();
}
}
QueryDSL
에서 Group By
를 사용하기 위해서는 EntityManager
를 통해 JPAQuery
를 직접 생성해서 사용해야 하는 단점이 있지만, QueryDSL
의 장점인 다양한 조건의 쿼리를 생성할 수 있다는 장점이 있습니다. (Optional
한 쿼리 조건을 각각 적용 가능)
230건의 데이터를 각 100회씩 호출했을 때 응답 시간은
Java Stream이 가장 속도가 느리며 나머지는 실제 쿼리가 비슷하기 때문에 평균 응답 시간도 같게 나왔습니다.
다만 JPQL
과 QueryDSL
은 초기 쿼리 파싱 때문에 Max 속도가 다른 것을 볼 수 있습니다.
출처 및 참고 : Spring Data JPA에서 GroupBy 처리하기
Spring Data JPA - @Query, 값, DTO 조회하기
[JPA] 엔티티 일부 데이터만 조회하는 Projection