JPA GroupBy 처리

크리링·2023년 3월 1일
0
post-thumbnail

DB에 있는 데이터를 복잡한 쿼리로 조회하고 결과 데이터를 또 다시 필요한 필드만으로 DTO로 바꾸는 과정을 깔끔하게 하는 방법이 어떤게 있을까?




Conditions

Database에 Sales에 대한 Table이 있다고 가정할 때

만들고자 하는 DTO는 다음과 같습니다.

(필드는 DB의 애트리뷰트의 자료형과 맞추어서 만들었습니다.)


@Getter
@NoArgs Constructor
@AllArgsConstructor
public class SalesCountDTO{

	private long id;
    
    private String name;
    
    private Long sales;
    
}



Java Stream 활용


...

	@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를 하기 때문에 데이터가 많아지면 느려지고 메모리 사용이 많아지기에 적절한 방식이 아닙니다.






JPQL 활용

JPAJPQLProjection을 이용해서 DTOQuery 결과값을 담는다.


@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 활용

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

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이 가장 속도가 느리며 나머지는 실제 쿼리가 비슷하기 때문에 평균 응답 시간도 같게 나왔습니다.

다만 JPQLQueryDSL은 초기 쿼리 파싱 때문에 Max 속도가 다른 것을 볼 수 있습니다.






출처 및 참고 : Spring Data JPA에서 GroupBy 처리하기
Spring Data JPA - @Query, 값, DTO 조회하기
[JPA] 엔티티 일부 데이터만 조회하는 Projection

0개의 댓글