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.foreach.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.foreach.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 Data Projection . Эта функциональность позволяет нам проецировать результаты запроса с помощью значительно меньшего количества кода .
4.1. Настройка результатов запросов JPA
Чтобы использовать проекцию на основе интерфейса, мы должны определить интерфейс 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 с функциями агрегирования. Во-первых, мы использовали стандарт JPA, включающий класс POJO, а во втором решении мы использовали облегченные проекции Spring Data с интерфейсом.
Проекции Spring Data позволяют нам писать меньше кода как на Java, так и на JPQL.
Как всегда, пример кода для этого руководства доступен на GitHub .