1. 개요

Spring Data JPA는 특정 상황에서 데이터베이스에서 엔터티를 검색하기 위해 쿼리 생성을 추상화할 수 있지만 때때로 집계 함수를 추가할 때와 같이 쿼리를 사용자 정의해야 합니다 .

이 사용방법(예제)에서는 이러한 쿼리의 결과를 개체로 변환하는 방법에 중점을 둘 것입니다. JPA 사양과 POJO를 포함하는 솔루션과 Spring Data Projection을 사용하는 솔루션 등 두 가지 솔루션을 살펴보겠습니다.

2. JPA 쿼리 및 집계 문제

JPA 쿼리는 일반적으로 매핑된 엔터티의 인스턴스로 결과를 생성합니다. 그러나 집계 함수가 있는 쿼리는 일반적으로 결과를 Object[] 로 반환합니다 .

문제를 이해하기 위해 게시물과 댓글 간의 관계를 기반으로 도메인 모델을 정의해 보겠습니다.

@Entity
public class Post {
    @Id
    private Integer id;
    private String title;
    private String content;
    @OneToMany(mappedBy = "post")
    private List comments;

    // additional properties
    // standard constructors, getters, and setters
}

@Entity
public class Comment {
    @Id
    private Integer id;
    private Integer year;
    private boolean approved;
    private String content;
    @ManyToOne
    private Post post;

    // additional properties
    // standard constructors, getters, and setters
}

우리 모델은 게시물에 많은 댓글이 있을 수 있고 각 댓글은 하나의 게시물에 속한다고 정의합니다. 이 모델과 함께 Spring Data Repository 를 사용해 봅시다 .

@Repository
public interface CommentRepository extends JpaRepository<Comment, Integer> {
    // query methods
}

이제 연도별로 그룹화된 댓글을 계산해 보겠습니다.

@Query("SELECT c.year, COUNT(c.year) FROM Comment AS c GROUP BY c.year ORDER BY c.year DESC")
List<Object[]> countTotalCommentsByYear();

 결과가 모양이 다르기 때문에 이전 JPA 쿼리의 결과 를 Comment 인스턴스로 로드할 수 없습니다 . 쿼리에 지정된 연도 및  COUNT 가 엔터티 개체와 일치하지 않습니다.

List에 반환된 범용 Object[] 의 결과에 여전히 액세스할 수 있지만 그렇게 하면 지저분하고 오류가 발생하기 쉬운 코드가 생성됩니다.

3. 클래스 생성자로 결과 사용자 지정

JPA 사양을 사용하면 개체 지향 방식으로 결과를 사용자 지정할 수 있습니다. 따라서 JPQL 생성자 표현식을 사용하여 결과를 설정할 수 있습니다.

@Query("SELECT new com.baeldung.aggregation.model.custom.CommentCount(c.year, COUNT(c.year)) "
  + "FROM Comment AS c GROUP BY c.year ORDER BY c.year DESC")
List<CommentCount> countTotalCommentsByYearClass();

이는 SELECT 문의 출력을 POJO에 바인딩합니다. 지정된 클래스에는 프로젝션된 속성과 정확히 일치하는 생성자가 있어야 하지만 @Entity 어노테이션을 달 필요는 없습니다 .

또한 JPQL에서 선언된 생성자는 정규화된 이름을 가져야 함을 알 수 있습니다.

package com.baeldung.aggregation.model.custom;

public class CommentCount {
    private Integer year;
    private Long total;

    public CommentCount(Integer year, Long total) {
        this.year = year;
        this.total = total;
    }
    // getters and setters
}

4. Spring Data Projection으로 결과 커스터마이징

또 다른 가능한 솔루션은 Spring Data Projection 을 사용하여 JPA 쿼리의 결과를 사용자 정의하는 것입니다 . 이 기능 을 사용하면 상당히 적은 코드로 쿼리 결과를 예상할 수 있습니다 .

4.1. JPA 쿼리 결과 사용자 지정

인터페이스 기반 프로젝션을 사용하려면 프로젝션된 속성 이름과 일치하는 getter 메서드로 구성된 Java 인터페이스를 정의해야 합니다. 쿼리 결과에 대한 인터페이스를 정의해 보겠습니다.

public interface ICommentCount {
    Integer getYearComment();
    Long getTotalComment();
}

이제 List<ICommentCount> 로 반환된 결과로 쿼리를 표현해 보겠습니다 .

@Query("SELECT c.year AS yearComment, COUNT(c.year) AS totalComment "
  + "FROM Comment AS c GROUP BY c.year ORDER BY c.year DESC")
List<ICommentCount> countTotalCommentsByYearInterface();

Spring이 투영된 값을 인터페이스에 바인딩할 수 있도록 하려면 인터페이스 에서 찾은 속성 이름을 가진 각 투영된 속성에 별칭을 제공해야 합니다 .

그런 다음 Spring Data는 즉시 결과를 구성하고 결과의 각 행에 대한 프록시 인스턴스 를 반환합니다.

4.2. 네이티브 쿼리 결과 사용자 지정

JPA 쿼리가 기본 SQL만큼 빠르지 않거나 데이터베이스 엔진의 특정 기능을 사용할 수 없는 상황에 직면할 수 있습니다. 이를 해결하기 위해 네이티브 쿼리를 사용합니다.

인터페이스 기반 프로젝션의 장점 중 하나는 기본 쿼리에 사용할 수 있다는 것입니다. ICommentCount 를 다시 사용 하여 SQL 쿼리에 바인딩해 보겠습니다.

@Query(value = "SELECT c.year AS yearComment, COUNT(c.*) AS totalComment "
  + "FROM comment AS c GROUP BY c.year ORDER BY c.year DESC", nativeQuery = true)
List<ICommentCount> countTotalCommentsByYearNative();

이는 JPQL 쿼리와 동일하게 작동합니다.

5. 결론

이 기사에서는 JPA 쿼리 결과를 집계 함수로 매핑하는 문제를 해결하기 위해 두 가지 솔루션을 평가했습니다. 먼저 POJO 클래스와 관련된 JPA 표준을 사용했습니다.

두 번째 솔루션의 경우 인터페이스와 함께 경량 Spring Data 프로젝션을 사용했습니다. Spring Data 프로젝션을 사용하면 Java와 JPQL 모두에서 더 적은 코드를 작성할 수 있습니다.

항상 그렇듯이 이 기사의 예제 코드는 GitHub에서 사용할 수 있습니다 .

Persistence footer banner