JPA часто подвергается критике за невозможность загружать сущности частично, что на самом деле является большим заблуждением. Spring Data JPA и Hibernate включают в себя множество инструментов по частичной загрузке сущностей.
Команда Spring АйО подготовила статью, в которой рассмотрела имеющиеся в Spring Data JPA инструменты для частичной загрузки сущностей, а также разобрала их особенности и corner-кейсы. Давайте попробуем рассмотреть все способы такой частичной загрузки сущностей на примере основных способов взаимодействия с Hibernate в Spring приложениях:
Spring Data JPA
EntityManager
Criteria API
Также существует проект Jakarta Data, который активно развивается. Однако на момент написания этой статьи вышла только первая стабильная версия 1.0.0. Рассматривать его не станем, пока не накопим достаточное количество реальных использований.
Интересует ли эта проблема сообщество
После очередного холивара в одном из Telegram-каналов про Java на тему того, как плох JPA и Hibernate в частности, как он неоптимизированно выполняет запросы и как много грузится лишних данных, я решил немного углубиться в эти вопросы и попытаться встать на защиту упомянутого выше стэка, отправившись в путешествие на Stackoverflow. Сделаем поиск по тегу [spring-data-jpa]
и отсортируем вопросы по популярности. Мы увидим, что вопрос Spring JPA selecting specific columns находится на шестом месте.
В этой статье мы постараемся ответить на этот вопрос максимально широко: рассмотрим не только самые простые случаи с базовыми атрибутами, но также окунемся и в мир JPA-ассоциаций.
Задача
В проекте будет рассматриваться следующая модель данных:
Нашей задачей является загрузка нескольких базовый полей + ToOne (в нашем случае это атрибут author, ссылающийся на сущность Post) ассоциации для каждого способа частичной загрузки. Предположим, мы хотим получить все статьи, заголовок которых содержит некоторый текст, причем поиск не чувствителен к регистру, т.е. contains with ignore case. Итого выгружаем Post: id, slug, title; User(author): id, username
.
Проверять результат мы будем в соответствующих тестах, итоговые запросы будут видны в консоли: лог, в котором отобразится SQL-запрос, сгенерированный силами Hibernate.
Тестовые данные
Создадим две записи Post
, проинициализировав базовые поля и привязав авторов. Сервис в котором создаются и удаляются данные InitTestDataService.
toOne
Всего был найден 21 способ частичной загрузки для поставленной нами задачи.
Эти способы включают в себя разные подходы написания запроса:
Spring Data Repository derived-methods
Spring Data Repository query-methods
Entity Manager
Criteria API
Тестовый класс в котором можно увидеть все тесты с комментариями - ToOneTest.
Предисловие
Стоит внести небольшую ясность перед нашим экспериментом. Derived
методы — это методы, автоматически реализуемые фреймворком на основе их имен, т.е. без явного указания аннотации @Query. Поскольку мы не указываем запрос явно, а значит у нас отсутствует возможность указать, какие именно атрибуты нам требуется загрузить, у нас есть всего один способ указания конкретных атрибутов для загрузки - это projection.
Сами же проекции бывают двух видов: основанные на интерфейсах (Interface-based Projections) и на классах (Class-based Projections). Interface-based Projections в свою очередь можно поделить на открытые и закрытые.
В закрытых проекциях геттеры объявляются явно:
interface NamesOnly {
String getFirstname();
String getLastname();
}
В случае открытых проекций значение геттеров интерфейсов могут высчитываться на основе SpEL выражения:
interface NamesOnly {
@Value("#{target.firstname + ' ' + target.lastname}")
String getFullName();
}
Их мы рассматривать не будем, т.к. в документации явно сказано, что для них оптимизация запроса производиться не будет:
Spring Data cannot apply query execution optimizations in this case, because the SpEL expression could use any attribute of the aggregate root.
Для загрузки ToOne-ассоциаций ставим задачу проверить два варианта с flatten (плоскими) атрибутами, с nested (вложенным) классом, а также с Tuple и Map.
Для ToMany имеет смысл проверять работу с плоскими атрибутами, однако в этом случае загрузка будет со сложностью n*m. Это означает, что наш эксперимент подходит и для ToOne, и для ToMany, однако записей в случае с ToMany выгрузится больше.
Почти все, что мы рассмотрим для ToOne, справедливо и для ToMany, однако Hibernate не способен мапить коллекционные атрибуты на DTO/Projection, в связи с чем Hibernate в силах выполнить только HQL, и для кейса с ToMany будет возвращено декартово произведение n*m. Иначе говоря, кроме выгрузки и мапинга результата нам придется еще схлопывать дубликаты записей. Если эта тема будет интересна, мы обязательно напишем дополнительный пост про частичную загрузку и с ToMany-ассоциациями. Однако с несколькими примерами можно ознакомиться в проекте в тестовом классе ToManyTest.
Все, что будет рассмотрено далее, отлично подойдет и для Embbeded-кейса, поэтому отдельно его рассматривать не имеет смысла
Derived-methods
Interface-based flat projection
Для данного кейса будем в качестве проекции использовать отдельно взятый интерфейс. Пожалуй, данный подход можно отнести к базовой концепции в контексте использования проекций.
Объявим в нашем репозитории метод:
public interface PostRepository extends JpaRepository<Post, Long> {
<T> List<T> findAllByTitleContainsIgnoreCase(String title, Class<T> projection);
}
Исключительно в целях удобства метод будет заточен под динамическую проекцию, чтобы не писать под каждую проекцию отдельный метод.
Создадим класс проекции:
public interface PostWithAuthorFlat {
Long getId();
String getSlug();
String getTitle();
Long getAuthorId();
String getAuthorUsername();
}
Протестируем решение, написав тест. Попробуем немного углубиться в работу проекции, воспользовавшись дебагом. Ставим breakpoint на нашем тесте:
Завершающим звеном по получению данных является работа класса TupleBackedMap, сам объект Tuple же будет содержать необходимую нам информацию. Чтобы посмотреть цепочку по получению данных, обозначим границу установкой breakpoint в методе получения значения из TupleBackedMap (org.springframework.data.jpa.repository.query.AbstractJpaQuery.TupleConverter.TupleBackedMap)
:
И наблюдаем следующую цепочку по получению данных:
Полученный прокси сначала дойдет до метода invoke
класса MapAccessingMethodInterceptor
:
В результате чего мы получаем объект класса Accessor
, который предоставляет доступ к самому propertyName
. Преобразованная строка будет передана в TupleBackedMap
. Далее полученное значение будет нам представлено из всеми нам знакомого объекта Tuple
:
Сам же объект Tuple
в свою очередь предоставляет нам полный доступ к ключу и значению.
Всю схему можно описать так:
Под проекцией лежит прокси, в прокси - прокси, в прокси лежит некий target-объект, который является объектом TupleBackedMap
.
Или так:
«На море на океане есть остров, на том острове дуб стоит, под дубом сундук зарыт, в сундуке — заяц, в зайце — утка, в утке — яйцо» в яйце игла — смерть Кощея!
Подводя итоги данного кейса можно сделать вывод о том, что Spring предоставляет конвертер, который сам маппит Tuple
на проекцию. Этот подход отлично подходит для решения поставленной нами задачи, ведь в запросе присутствуют только те колонки, которые указаны в проекции:
Hibernate:
select
p1_0.id,
p1_0.slug,
p1_0.title,
p1_0.author_id,
a1_0.username
from
posts p1_0
left join
users a1_0
on a1_0.id=p1_0.author_id
where
upper(p1_0.title) like upper(?) escape '\'
Interface-based nested interface projections
Следующим способом для решения нашей задачи могло быть решение, основанное на использовании проекции, имеющей внутри себя еще одну проекцию, для получения данных об авторе поста. Это и есть тот самый nested кейс.
public interface PostWithAuthorNested {
Long getId();
String getSlug();
String getTitle();
UserPresentation getAuthor();
}
Для базовых полей будет загружено только то, что указано в проекции. Мы получим необходимые нам id
, slug
, title
. Однако для вложенного объекта будут загружены абсолютно все поля, что конечно, противоречит нашим требованиям. Проблема известна и даже имеет официальный ответ.
Стоит обратить внимание, что PostWithAuthorNested
это все тот же прокси вокруг TupleBackedMap
, а вот сам вложенный объект UserPresentation
является прокси непосредственно вокруг самой сущности User
:
Hibernate:
select
p1_0.id,
p1_0.slug,
p1_0.title,
a1_0.id,
a1_0.bio,
a1_0.email,
a1_0.image,
a1_0.password,
a1_0.token,
a1_0.username
from
posts p1_0
left join
users a1_0
on a1_0.id=p1_0.author_id
where
upper(p1_0.title) like upper(?) escape '\'
Подводя итоги: подход работает неоптимально, поставленную нами задачу не решает.
Class-based flat projections
Идем далее и следующим вариантом решения задачи по частичной выгрузке полей сущности можно отметить использования в качестве проекции отдельного Record
-класса (метод репозитория остается прежним)
public record PostWithAuthorFlatDto(Long id,
String slug,
String title,
Long authorId,
String authorUsername) {
}
Сам тест:
@Test
void derivedMethodClassFlatPrj() {
var posts = postRepository.findAllByTitleContainsIgnoreCase(
"spring",
PostWithAuthorFlatDto.class
);
assertEquals(1, posts.size());
var postFirst = posts.getFirst();
assertEquals(POST1_SLUG, postFirst.slug());
assertEquals(POST1_AUTHOR_NAME, postFirst.authorUsername());
}
Spring Data JPA передает result type в Hibernate, который в свою очередь производит мапинг. На плечах Spring'а только лишь грамотное формирование JPQL запроса. Этот вариант нас полностью устраивает.
Результат:
Hibernate:
select
p1_0.id,
p1_0.slug,
p1_0.title,
p1_0.author_id,
a1_0.username
from
posts p1_0
left join
users a1_0
on a1_0.id=p1_0.author_id
where
upper(p1_0.title) like upper(?) escape '\'
Class-based nested dto projections
Идем хорошим темпом, однако стоило бы рассмотреть и негативные, нерабочие сценарии, чтобы лучше понять логику работы с проекциями. Создадим такой record класс с вложенной DTO-проекцией:
public record PostWithAuthorNestedDto(Long id,
String slug,
String title,
UserPresentationDto author) {
}
Вроде, все должно заработать, однако при попытке "взлететь" мы получим ошибку:
Cannot set field 'author' to instantiate 'io.spring.jpa.projection.PostWithAuthorNestedDto'
О чем нас предупреждали:
В данном случае вся логика лежит на стороне Hibernate, Spring Data здесь не оказывает никакого влияния.
Class-based nested entity projections
Мы также можем указать для record целую сущность. Однако данный способ обязывает нас получить все поля из вложенной сущности. Придется положить в копилку еще один негативный кейс. Не наш вариант, но знать про это, кажется, было бы полезно:
public record PostWithAuthorEntity(Long id,
String slug,
String title,
User author) {
}
Получаем:
Hibernate:
select
p1_0.id,
p1_0.slug,
p1_0.title,
a1_0.id,
a1_0.bio,
a1_0.email,
a1_0.image,
a1_0.password,
a1_0.token,
a1_0.username
from
posts p1_0
left join
users a1_0
on a1_0.id=p1_0.author_id
where
upper(p1_0.title) like upper(?) escape '\'
Query-methods
А теперь давайте рассмотрим способ, когда мы можем явно в запросе указывать, какие атрибуты нам нужно выгрузить, т.е. к Query
-методам. В этом случае мы сразу же решаем одну немаловажную задачу - явно указываем то, что хотим получить. В нашей зоне ответственности остается только лишь правильно реализовать маппинг.
Interface-based flat projections
Spring богат своими возможностями, потому он, например, может обработать JPQL
который мы написали самостоятельно, а затем и вовсе сформировать TupleBackedMap
.
Данный способ интересен тем, что нам ничего не мешает написать обычный JPQL
запрос, а в качестве возвращаемого значения указать проекцию:
@Query("""
select a.id as id,
a.slug as slug,
a.title as title,
a.author.id as authorId,
a.author.username as authorUsername
from Post a
where lower(a.title) like lower(concat('%', ?1, '%'))""")
List<PostWithAuthorFlat> findAllPostWithAuthorFlat(String title);
Сама проекция:
public interface PostWithAuthorFlat {
Long getId();
String getSlug();
String getTitle();
Long getAuthorId();
String getAuthorUsername();
}
Ключевым моментом формирования JPQL
-запроса является требование по указанию алиасов: они обязательно должны быть такими же, как свойства в проекции, иначе магии маппинга не произойдет. Кстати, как и ожидалось, под интерфейсом лежит все та же прокси с TupleBackedMap
.
Сам результат:
Hibernate:
select
p1_0.id,
p1_0.slug,
p1_0.title,
p1_0.author_id,
a1_0.username
from
posts p1_0
join
users a1_0
on a1_0.id=p1_0.author_id
where
lower(p1_0.title) like lower(('%'||?||'%')) escape ''
Class-based flat projections
Данный способ по частичному получению данных является базовым для Hibernate. Нужно всего лишь создать класс-проекцию с конструктором. После чего инициализируем этот класс прямо в JPQL. Алиасы в этом случае нам не требуются. Прокси в этом случае также не создается, используется только DTO-проекция.
@Query("""
select
new io.spring.jpa.projection.PostWithAuthorFlatDto(
a.id,
a.slug,
a.title,
a.author.id,
a.author.username
)
from Post a
where lower(a.title) like lower(concat('%', ?1, '%'))""")
List<PostWithAuthorFlatDto> findAllPostWithAuthorFlatDto(String title);
Проекция:
public record PostWithAuthorFlatDto(Long id,
String slug,
String title,
Long authorId,
String authorUsername) {
}
Тест:
@Test
void queryMethodClassFlat() {
var posts = postRepository.findAllPostWithAuthorFlatDto("spring");
assertEquals(1, posts.size());
PostWithAuthorFlatDto post = posts.getFirst();
assertEquals(POST1_SLUG, post.slug());
assertEquals(POST1_AUTHOR_NAME, post.authorUsername());
}
Успех:
Hibernate:
select
p1_0.id,
p1_0.slug,
p1_0.title,
p1_0.author_id,
a1_0.username
from
posts p1_0
join
users a1_0
on a1_0.id=p1_0.author_id
where
lower(p1_0.title) like lower(('%'||?||'%')) escape ''
Class-based nested class projections
Важной особенностью метода с использованием вложенных проекций является то, что можно делать сколько угодно вложенностей. Поскольку мы используем HQL, то можем инициализировать нашу DTO как нам удобно. В том числе и создавая новые DTO объекты внутри DTO.
Напишем такой метод:
@Query("""
select
new io.spring.jpa.projection.PostWithAuthorNestedDto(
a.id,
a.slug,
a.title,
new io.spring.jpa.projection.UserPresentationDto(
a.author.id,
a.author.username
)
)
from Post a
where lower(a.title) like lower(concat('%', ?1, '%'))""")
List<PostWithAuthorNestedDto> findAllPostWithAuthorNestedDto(String title);
А также пару проекций под него: проекцию для самого поста, внутри нее объявляем проекцию для автора.
public record PostWithAuthorNestedDto(Long id,
String slug,
String title,
UserPresentationDto author) {
}
Проекция для автора:
public record UserPresentationDto(Long id, String username) {
}
Напишем простой тест:
@Test
void queryMethodClassNested() {
var posts = postRepository.findAllPostWithAuthorNestedDto("spring");
assertEquals(1, posts.size());
PostWithAuthorNestedDto post = posts.getFirst();
assertEquals(POST1_SLUG, post.slug());
assertEquals(POST1_AUTHOR_NAME, post.author().username());
}
Непосредственно запрос:
Hibernate:
select
p1_0.id,
p1_0.slug,
p1_0.title,
p1_0.author_id,
a1_0.username
from
posts p1_0
join
users a1_0
on a1_0.id=p1_0.author_id
where
lower(p1_0.title) like lower(('%'||?||'%')) escape ''
Tuple, Object[], List<>, Map
Hibernate также позволяет возвращать select expression, которые мы можем написать в виде Object[]
, Tuple
, Map
, List
. Подробно останавливаться на каждом не станем, разница лишь в возвращаемом значении:
@Query("""
select
a.id as id,
a.slug as slug,
a.title as title
from Post a
where lower(a.title) like lower(concat('%', ?1, '%'))""")
List<Tuple> findAllTupleBasic(String title);
@Query("""
select
a.id as id,
a.slug as slug,
a.title as title
from Post a
where lower(a.title) like lower(concat('%', ?1, '%'))""")
List<Object[]> findAllObjectWithAuthor(String title);
@Query("""
select
a.id as id,
a.slug as slug,
a.title as title
from Post a
where lower(a.title) like lower(concat('%', ?1, '%'))""")
List<List<?>> findAllListWithAuthor(String title);
@Query("""
select
a.id as id,
a.slug as slug,
a.title as title
from Post a
where lower(a.title) like lower(concat('%', ?1, '%'))""")
List<Map<String, Object>> findAllMapWithAuthor(String title);
P.S.: для Derived
-методов аналогично можно указывать такие же возвращаемые значения. Spring Data все возьмет на себя и выгрузит все поля.
Entity Manager
Все способы, описанные для @Query
, работают и в Entity Manager, кроме Interface-based projection, так как Interface-based projection является концепцией самого Spring, сам же Hibernate ничего про нее не знает.
Criteria API
Criteria API используется для создания типобезопасных и гибких запросов к базе данных на языке Java. Сам по себе Criteria API является "type-safe alternative to HQL".
Мы будем использовать стандартные функции CriteriaBuilder
, однако, если возникнет потребность в дополнительных функциях из HibernateCriteriaBuilder, приведенные ниже примеры будут актуальны и для него.
Для начала нам нужно указать атрибуты, которые мы собираемся передать в запрос. Имя атрибута мы можем очень удобно указать с помощью константы, которая была сгенерирована автоматически с использованием зависимости:
dependencies {
annotationProcessor 'org.hibernate:hibernate-jpamodelgen:{version}'
}
var idPath = owner.<Long>get(Post_.ID);
var slugPath = owner.<String>get(Post_.SLUG);
var titlePath = owner.<String>get(Post_.TITLE);
var authorIdPath = owner.get(Post_.AUTHOR).<Long>get(User_.ID);
var authorUsernamePath = owner.get(Post_.AUTHOR).<String>get(User_.USERNAME);
В последней документации Hibernate данный способ используется во всех примерах, из чего можно сделать вывод, что это "тихая" рекомендация.
По итогу константы всегда будут перегенерированы и всегда будут находиться в актуальном состоянии, в результате чего мы выигрываем в безопасности и надежности приложения.
DTO
Итак, мы объявили path'ы, остается лишь написать запрос, закинуть результат в коллекцию, после чего убедиться в жизнеспособности подхода:
@Test
void criteriaDto() {
var cb = em.getCriteriaBuilder();
var query = cb.createQuery(PostWithAuthorFlatDto.class);
var owner = query.from(Post.class);
var idPath = owner.<Long>get(Post_.ID);
var slugPath = owner.<String>get(Post_.SLUG);
var titlePath = owner.<String>get(Post_.TITLE);
var authorIdPath = owner.get(Post_.AUTHOR).<Long>get(User_.ID);
var authorUsernamePath = owner.get(Post_.AUTHOR).<String>get(User_.USERNAME);
query.multiselect(idPath, slugPath, titlePath, authorIdPath, authorUsernamePath)
.where(cb.like(cb.lower(titlePath), "%spring%"));
var resultList = em.createQuery(query).getResultList();
for (PostWithAuthorFlatDto post : resultList) {
assertEquals(POST1_SLUG, post.slug());
assertEquals(POST1_AUTHOR_NAME, post.authorUsername());
}
}
Проекция:
public record PostWithAuthorFlatDto(Long id,
String slug,
String title,
Long authorId,
String authorUsername) {
}
В данном случае мы возвращаем множество элементов (атрибутов) и мапим их на DTO, которое указываем при создании query
jakarta.persistence.criteria.CriteriaBuilder#createQuery(java.lang.Class)
Успешный успех:
Hibernate:
select
p1_0.id,
p1_0.slug,
p1_0.title,
p1_0.author_id,
a1_0.username
from
posts p1_0
join
users a1_0
on a1_0.id=p1_0.author_id
where
lower(p1_0.title) like ? escape ''
К минусам работы через DTO можно отнести только то, что сохраняется риск случайного рефактиринга DTO. В этом случае мы поймаем ошибку, что не нашлось, например, подходящего конструктора. Для решения этой проблемы рассмотрим вариант с использованием Tuple
.
Tuple
В случае с Tuple
инициализация объекта происходит ручным способом и мы попросту не сможем создать сам объект и, конечно, поймаем ошибку ещё до запуска приложения на этапе компиляции.
Представляем вашему вниманию, пожалуй, самый безопасный способ выполнения частичных запросов:
@Test
void criteriaTuple() {
var cb = em.getCriteriaBuilder();
var query = cb.createTupleQuery();
var owner = query.from(Post.class);
var idPath = owner.<Long>get(Post_.ID);
var slugPath = owner.<String>get(Post_.SLUG);
var titlePath = owner.<String>get(Post_.TITLE);
var authorIdPath = owner.get(Post_.AUTHOR).<Long>get(User_.ID);
var authorUsernamePath = owner.get(Post_.AUTHOR).<String>get(User_.USERNAME);
query.select(cb.tuple(idPath, slugPath, titlePath, authorIdPath, authorUsernamePath))
.where(cb.like(cb.lower(titlePath), "%spring%"));
var resultList = em.createQuery(query).getResultList().stream()
.map(tuple -> new PostWithAuthorNestedDto(
tuple.get(idPath),
tuple.get(slugPath),
tuple.get(titlePath),
new UserPresentationDto(
tuple.get(authorIdPath),
tuple.get(authorUsernamePath)
)
)).toList();
for (PostWithAuthorNestedDto post : resultList) {
assertEquals(POST1_SLUG, post.slug());
assertEquals(POST1_AUTHOR_NAME, post.author().username());
}
}
Сами проекции:
public record PostWithAuthorNestedDto(Long id,
String slug,
String title,
UserPresentationDto author) {
}
public record UserPresentationDto(Long id, String username) {
}
После формирования Path мы достаем значения из Tuple
и складываем в наше DTO (если не требуются дополнительные манипуляции с выборкой). Таким образом мы получаем не только "type-safe alternative to HQL", но и безопасную и контролируемую работу с результатом нашего запроса. Концепция очень похожа на работу с Tuple
в библиотеке QueryDSL.
В результате получаем запрос, удовлетворяющий наши требования. Остальные примеры для Criteria API рассмотрены вот тут. Работает все максимально типично относительно случаев, рассмотренных выше.
CriteriaDefinition в Hibernate 6.3+
UPD: Начиная с версии 6.3, Hibernate позволяет нам использовать вспомогательный класс CriteriaDefinition
для уменьшения многословности criteria-запросов.
Пример с DTO будет выглядеть следующим образом:
@Test
void criteriaDefinitionDto() {
var query = new CriteriaDefinition<>(em, PostWithAuthorFlatDto.class) {{
var owner = from(Post.class);
var titlePath = owner.<String>get(Post_.TITLE);
var authorPath = owner.get(Post_.AUTHOR);
multiselect(
owner.get(Post_.ID),
owner.get(Post_.SLUG),
titlePath,
authorPath.get(User_.ID),
authorPath.get(User_.USERNAME)
).where(like(lower(titlePath), "%spring%"));
}};
var resultList = em.createQuery(query).getResultList();
assertEquals(1, resultList.size());
for (var post : resultList) {
assertEquals(POST1_SLUG, post.slug());
assertEquals(POST1_AUTHOR_NAME, post.authorUsername());
}
}
Пример с Tuple:
@Test
void criteriaDefinitionTuple() {
var query = new CriteriaDefinition<>(em, Tuple.class) {};
var owner = query.from(Post.class);
var idPath = owner.<Long>get(Post_.ID);
var slugPath = owner.<String>get(Post_.SLUG);
var titlePath = owner.<String>get(Post_.TITLE);
var authorPath = owner.get(Post_.AUTHOR);
var authorIdPath = authorPath.<Long>get(User_.ID);
var authorUsernamePath = authorPath.<String>get(User_.USERNAME);
query.where(query.like(query.lower(titlePath), "%spring%"))
.multiselect(idPath, slugPath, titlePath, authorIdPath, authorUsernamePath);
var resultList = em.createQuery(query).getResultList().stream()
.map(tuple -> new PostWithAuthorNestedDto(
tuple.get(idPath),
tuple.get(slugPath),
tuple.get(titlePath),
new UserPresentationDto(
tuple.get(authorIdPath),
tuple.get(authorUsernamePath)
)
)).toList();
assertEquals(1, resultList.size());
for (var post : resultList) {
assertEquals(POST1_SLUG, post.slug());
assertEquals(POST1_AUTHOR_NAME, post.author().username());
}
}
Ознакомиться с результатами работы всех способов вы можете в репозитории.
Кстати, до 6 версии Hibernate Criteria API
-запросы к сущностям выполнялись следующим образом:
Criteria API генерировал обычный JPQL, Hibernate же в свою очередь проводил его анализ в соответствии с грамматикой HQL, только затем генерировался SQL-запрос.
Начиная с Hibernate 6 Criteria API сразу преобразуется в SQM:
Подробнее про SQM можно почитать тут.
Выводы
Если мы пишем HQL/JPQL query, то мы контролируем запрос и возвращаем только то что мы хотим. Вопрос только в том, как мапить.
Если мы пишем HQL/JPQL всегда можно вернуть
Tuple
илиMap
и помапить с него на DTO."Использовать ли repository derived method?" - каждый решает сам. В простых случаях и HQL будет простым, в сложных - длина имени метода будет стремиться выйти за пределы нашей солнечной системы. В рамках решаемых задач для этого проекта, конкретно для частичной выгрузки данных, repository derived method выглядит самым небезопасным. Мы не можем контролировать запрос, а HQL может поменяться из-за изменения DTO/Projection или самой Entity.
Когда мы работаем с
Tuple
, очень удобным решением является использование библиотеки hibernate-jpamodelgen, что позволяет нам пользоваться автосгенерированными константами. В последней документации Hibernate данный способ используется во всех примерах, можно сказать, что это "тихая" рекомендация. Также, используя эти константы, легко создавать jakarta.persistence.criteria.Path для Criteria API.Когда мы пишем Query в Spring Data и используем DTO, по дефолту будут валидироваться выражения с DTO: будут проверены как типы, так и количество аргументов в конструкторе. А самое главное - никакой прокси-магии.
Не знаете что вернуть? Верните
Tuple
. Это очень удобно.HQL + Class-Based Projection, он же DTO, он же select new class конструкция работают всегда, включая вложенные классы.
Для ToMany, при выгрузки данных в виде Projection/DTO/Tuple нам придется решать вопрос о мердже дублирующихся данных.
Присоединяйтесь к русскоязычному сообществу разработчиков на Spring Boot в телеграм — Spring АйО, чтобы быть в курсе последних новостей из мира разработки на Spring Boot и всего, что с ним связано.
Ждем всех, присоединяйтесь!
Комментарии (8)
Green21
14.08.2024 11:02+1Когда мы работаем с
Tuple
, очень удобным решением является использование библиотеки hibernate-jpamodelgen, что позволяет нам пользоваться автосгенерированными константами.и не только с Tuple, а вообще, в принципе, лучше генерировать так называемую метамодель данных чтобы запросы были type-safe. Т.е. вместо строковых названий таблиц/столбцов использовать сгенерированную метамодель.
Green21
14.08.2024 11:02+1Ну и лучше использовать
HibernateCriteriaBuilder
вместоCriteriaBuilder
HibernateCriteriaBuilder builder = (HibernateCriteriaBuilder) em.getCriteriaBuilder();
Slobodator
14.08.2024 11:02Vlad Mihalchea предлагает способ, как писать
select new PostWithAuthorFlatDto
без указания полного имени класса.alexander-shustanov
14.08.2024 11:02+3Но для этого нужно объявить свой IntegratorProvider, в котором перечислить все такие классы.
public class ClassImportIntegratorIntegratorProvider implements IntegratorProvider { @Override public List<Integrator> getIntegrators() { return List.of( new ClassImportIntegrator( List.of( PostDTO.class ) ) ); } } spring.jpa.properties.hibernate.integrator_provider=your.awesome.app.ClassImportIntegratorIntegratorProvider
Лично мне кажется не супер полезной функциональностью
Gmugra
14.08.2024 11:02Любая критика JPA в своей сути о том, что это экстремально сложная спецификация.
К которой добавляются экстремально сложные реализации, которые не во всем спецификации следуют и к тому же тащат с собой сотни расширений.
И как вишенка на торте: дополнительные библиотеки Spring Data, DeltaSpike Data и т.п.
Всё это вместе: тысячи страниц документаций, сотни багов, пачки "воркараундов" ...
Нужны годы, буквально, опыта чтобы умело с этим работать и в любовно расставленные повсеместно ловушки не попадать.
Вот вы целую хорошую статью написали по сути о том, как не попасть в засаду выполняя тривиальный запрос.
А в соседней статье целый опус о том как не просто реализовать equals(!) для Entity.
Я о 10-ках подобных нюансов JPA мог бы рассказать, и это при том, что я убежден: за много лет я сталкивался я с каким-то небольшим подмножеством проблем.
И люди тащат этого монстра в микросервис с 5-ю запросами к базе.
Ну такое...
Green21
Еще есть такая прикольная штука как CriteriaDefinition:
spring_aio Автор
Добавили блок. Спасибо!