Personalizando o resultado de consultas JPA com funções de agregação

Personalizando o resultado de consultas JPA com funções de agregação

1. Visão geral

Enquanto Spring Data JPA pode abstrair a criação de consultas para recuperar entidades do banco de dados em situações específicas,we sometimes need to customize our queries, such as when we add aggregation functions.

Neste tutorial, vamos nos concentrar em como converter os resultados dessas consultas em um objeto. Exploraremos duas soluções diferentes - uma envolvendo a especificação JPA e um POJO, e outra usando Spring Data Projection.

2. Consultas JPA e o problema de agregação

As consultas JPA normalmente produzem seus resultados como instâncias de uma entidade mapeada. No entanto,queries with aggregation functions normally return the result as Object[].

Para entender o problema, vamos definir um modelo de domínio com base na relação entre postagens e comentários:

@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
}

Nosso modelo define que uma postagem pode ter muitos comentários e cada comentário pertence a uma postagem. Vamos usar umSpring Data Repository com este modelo:

@Repository
public interface CommentRepository extends JpaRepository {
    // query methods
}

Agora, vamos contar os comentários agrupados por ano:

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

The result of the previous JPA query cannot be loaded into an instance of Comment, because the result is a different shape. OsyeareCOUNT especificados na consulta não correspondem ao nosso objeto de entidade.

Embora ainda possamos acessar os resultados nosObject[] de uso geral retornados na lista, isso resultará em um código confuso e sujeito a erros.

3. Personalizando o resultado com construtores de classe

The JPA specification allows us to customize results in an object-oriented fashion. Portanto, podemos usar uma expressão de construtor JPQL para definir o resultado:

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

Isso vincula a saída da instruçãoSELECT a um POJO. The class specified needs to have a constructor that matches the projected attributes exactly, but it’s not required to be annotated with @Entity.

Também podemos ver que o construtor declarado no JPQL deve ter um nome completo:

package com.example.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. Personalizando o resultado com projeção de dados de primavera

Outra solução possível é customizar o resultado das consultas JPA comSpring Data Projection. Esta funcionalidadeallows us to project query results with considerably less code.

4.1. Personalizando o resultado de consultas JPA

Para usar a projeção baseada em interface, devemos definir uma interface Java composta por métodos getter que correspondam aos nomes dos atributos projetados. Vamos definir uma interface para o resultado da nossa consulta:

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

Agora, vamos expressar nossa consulta com o resultado retornado comoList<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 countTotalCommentsByYearInterface();

To allow Spring to bind the projected values to our interface, we need to give aliases to each projected attribute com o nome da propriedade encontrada na interface.

O Spring Data então construirá o resultado em tempo real e retornará umproxy instance para cada linha do resultado.

4.2. Personalizando o resultado de consultas nativas

Podemos enfrentar situações em que as consultas JPA não são tão rápidas quanto o SQL nativo ou não podem usar alguns recursos específicos do nosso mecanismo de banco de dados. Para resolver isso, usamos consultas nativas.

One advantage of interface-based projection is that we can use it for native queries. Vamos usarICommentCount novamente e vinculá-lo a uma consulta 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 countTotalCommentsByYearNative();

Isso funciona de forma idêntica às consultas JPQL.

5. Conclusão

Neste artigo,we evaluated two different solutions to address mapping the results of JPA Queries with aggregation functions. Primeiro, usamos o padrão JPA, envolvendo uma classe POJO, e na segunda solução, usamos as projeções leves do Spring Data com uma interface.

As projeções do Spring Data permitem escrever menos código, tanto em Java quanto em JPQL.

Como sempre, o código de exemplo para este tutorial está disponívelover on GitHub.