- О проекте
- Интересует ли эта проблема сообщество
- Задача
- Тестовые данные
- Тестирование
- Выводы
- Getting Started
- Built With
- TODO
JPA часто предъявляют за невозможность загружать сущности частично, что на самом деле является большим заблуждением. Spring Data JPA & Hibernate включают в себя множество инструментов по частичной загрузке сущностей. В рамках исследования рассмотрим имеющиеся в Spring Data JPA инструменты, разберем их особенности и посмотрим на corner case. Давайте попробуем рассмотреть все способы такой частичной загрузки сущностей. Рассмотрим на примере основых способов взаимодействия с Hibernate в Spring приложениях:
- Spring Data JPA
- EntityManager
- Criteria API
Существует еще проект Jakarta Data, который активно развивается. Пока вышла только первая стабильная версия 1.0.0 и мы не будем его рассматривать в данном эксперименте.
О да, еще как интересует! Для этого отправимся в путешествие на stackoverflow. Напишем в поиске тег [spring-data-jpa] и отсортируем по популярности вопросы. Мы увидим, что вопрос Spring JPA selecting specific columns находится на шестом месте. И в этом проекте мы постараемся ответить на этот вопрос максимально полно. Наш обзор будет касаться не только загрузки базовых атрибутов, но мы также окунемся в мир ассоциаций JPA.
В данном проекте будет рассматриваться следующая модель данных:
classDiagram
Post --> User: author
Post --> User: likeUsers
class Post {
Long id
String slug
String tittle
String body
Instant createAt
Instant updatedAt
User author
Se~User~ likeUsers
}
class User {
Long id
String email
String password
String token
String username
String bio
byte[] image
}
Наша задача для каждого способа частичной загрузки попробовать загрузить следующие данные:
- Несколько базовый полей из сущности Author - id, slug, tittle.
- Несколько базовый полей + ToOne ассоциация. В нашем случае это author: User - id, username
- Несколько базовый полей + ToMany ассоциация. Для likeUsers также будем загружать id, username.
Запрос буде простой - найти все статьи заголовок которых содержит заданный текст. Поиск не чувствительный к регистру, т.е. contains with ignore case.
Проверять результат мы будем в соответствующих тестах, результат запроса будут видны в консоли. Специальный анализ логов, который бы показывал что выбираются только те поля что мы хотим, я не писал. Просто смотрю в логи, какой sql генерирует hibernate.
Создадим две записи Post c двумя подписчиками и автором. Сервис в котором создаются и удаляются данные InitTestDataService
Под derived method будем понимать, запросы которые основаны на имени метода, т.е. без явного указания аннотации @Query. В данном случае, у нас есть всего один способ как мы можем указать какие атрибуты мы хотим загрузить, это projection. Но проекции бывают двух видов, основанные на интерфейсах (Interface-based Projections) и на классах (Class-based Projections DTOs). Еще есть, так называемые Open Projections, где значение гетеров интерфейсов могу высчитываться на основе 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( вложенным) классом. Для ToMany все устроенно чуть сложнее, там также может быть кейс с nested и flat, но сам hibernate не умеет мапить вложенные коллекции, он вернет плоские записи и нам самостоятельно придется их помапить.
Важно, в тестах будут приведены только рабочие варианты, т.е. те которые успешно справляются с поставленной задачей, т.е. частичной загрузкой. Примеры в которых можно загрузить данные, но они выгружают больше данных чем нужно, рассматриваться не будут.
Всего я нашел 17 способов частичной загрузки для кейса когда нам надо загрузить только basic attributes. Эти способы включают в себя разные подходы написания запроса:
- Repository derived methods
- Repository query methods
- Entity Manager
- Criteria API
Тестовый класс в котором можно увидеть все тесты с комментариями - BasicAttributesTest. Поскольку проект находится в стадии разработки, могут дополняться или убираться методы, но в конечном варианте должны соответствовать следующему списку:
- Repository derived methods. Interface-based projections
- Repository derived methods. Class-based projections
- Repository query methods. Interface-based projections
- Repository query methods. Class-based projections
- Repository query methods. Object Array
- Repository query methods. Tuple
- Repository query methods. Map (select new map)
- Repository query methods. List (select new list)
- Entity Manager. Object Array
- Entity Manager. Tuple
- Entity Manager. Class-based Projections (select new)
- Entity Manager. Map (select new map)
- Entity Manager. List (select new list)
- Criteria API. Object Array
- Criteria API. Tuple
- Criteria API. Class-based Projections
- Criteria API. List
Тестовый класс в котором можно увидеть все тесты с комментариями - ToOneTest.
Рассматриваются следующие кейсы:
- Repository derived methods. Interface-based flat projections
- Repository derived methods. Interface-based nested projections. Это как раз тот случай когда запрос выполняется, но работает не правильно, т.е. загружает больше полей чем надо.
- Repository derived methods. Class-based flat projections
- Repository query methods. Interface-based flat projections
- Repository query methods. Interface-based with nested dto class projections.
- Repository query methods. Class-based flat projections
- Repository query methods. Class-based nested class projections
- Repository query methods. Class-based nested map projections
- Repository query methods. Tuple
- Repository query methods. Map (select new map)
- Repository query methods. Object Array
- Repository query methods. List (select new list)
- Entity Manager. Class-based Projections (select new)
- Entity Manager. Tuple
- Entity Manager. Map (select new map)
- Entity Manager. Object Array
- Entity Manager. List (select new list)
- Criteria API. Tuple
- Criteria API. Class-based Projections
- Criteria API. Object Array
- Criteria API. List (list)
Тестовый класс в котором можно увидеть все тесты с комментариями - ToManyTest.
Рассматриваются следующие кейсы:
- Repository derived methods. Interface-based projections
- Repository derived methods. Class-based projections
- Repository query methods. Interface-based nested projections
- Repository query methods. Class-based flat projections
- Repository query methods. Class-based nested projections
- Repository query methods. Object Array
- Repository query methods. Tuple
- Repository query methods. Map (select new map)
- Repository query methods. List (select new list)
- Entity Manager. Class-based Projections (select new)
- Entity Manager. Tuple
- Entity Manager. Map (select new map)
- Entity Manager. Object Array
- Entity Manager. List (select new list)
- Criteria API. Tuple
- Criteria API. Object Array
- Criteria API. Class-based Projections
- Criteria API. List (select new list)
- Если мы пишем 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
- Java 21
- Spring Data JPA
- Hibernate
- H2
- PostgreSQL
- Junit
- Clone project
- Go to project directory
- Run project tests
./gradlew test
- Надо ли рассматривать Embedded?
- Надо ли рассматривать JPA Specification и его проблемы?
- Надо ли рассматривать Pagination?
- Кастом мапинг, кажется вообще не работает. Надо ли об этом говорить? org.springframework.core.convert.ConverterNotFoundException: No converter found capable of converting from type [org.springframework.data.jpa.repository.query.AbstractJpaQuery$TupleConverter$TupleBackedMap] to type [io.amplicode.jpa.projection.PostBasicDto] В классе org.springframework.data.repository.query.ResultProcessor.ProjectingConverter в котором и происходит конвертация resultType есть ConversionService, но всегда инициализируется как DefaultConversionService.getSharedInstance() и его не очень то и пополнишь(
- Как правильно называется способ писать запрос на основе имени метода - "Repository derived methods"?