«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 с этой моделью:

@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, указанные в запросе, не соответствуют нашему объекту сущности.

Хотя мы все еще можем получить доступ к результатам в 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

Другое возможное решение — настроить результат запросов JPA. с проекцией данных Spring. Эта функциональность позволяет нам проецировать результаты запроса со значительно меньшим количеством кода.

4.1. Настройка результатов запросов JPA

Чтобы использовать проекцию на основе интерфейса, мы должны определить интерфейс Java, состоящий из методов получения, которые соответствуют именам проецируемых атрибутов. Давайте определим интерфейс для нашего результата запроса:

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

Теперь давайте выразим наш запрос с результатом, возвращаемым как List\u003cICommentCount\u003e:

@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 с функциями агрегирования. Во-первых, мы использовали стандарт JPA, включающий класс POJO, а во втором решении мы использовали облегченные проекции Spring Data с интерфейсом.

Проекции Spring Data позволяют нам писать меньше кода как на Java, так и на JPQL.

Как всегда, пример кода для этого руководства доступен на GitHub.