Skip to content

comru/jpa-partial-load

Repository files navigation

JPA Entity. Загрузи меня не полностью!

Содержание

О проекте

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

Наша задача для каждого способа частичной загрузки попробовать загрузить следующие данные:

  1. Несколько базовый полей из сущности Author - id, slug, tittle.
  2. Несколько базовый полей + ToOne ассоциация. В нашем случае это author: User - id, username
  3. Несколько базовый полей + 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 не умеет мапить вложенные коллекции, он вернет плоские записи и нам самостоятельно придется их помапить.

Важно, в тестах будут приведены только рабочие варианты, т.е. те которые успешно справляются с поставленной задачей, т.е. частичной загрузкой. Примеры в которых можно загрузить данные, но они выгружают больше данных чем нужно, рассматриваться не будут.

Basic attributes

Всего я нашел 17 способов частичной загрузки для кейса когда нам надо загрузить только basic attributes. Эти способы включают в себя разные подходы написания запроса:

  • Repository derived methods
  • Repository query methods
  • Entity Manager
  • Criteria API

Тестовый класс в котором можно увидеть все тесты с комментариями - BasicAttributesTest. Поскольку проект находится в стадии разработки, могут дополняться или убираться методы, но в конечном варианте должны соответствовать следующему списку:

  1. Repository derived methods. Interface-based projections
  2. Repository derived methods. Class-based projections
  3. Repository query methods. Interface-based projections
  4. Repository query methods. Class-based projections
  5. Repository query methods. Object Array
  6. Repository query methods. Tuple
  7. Repository query methods. Map (select new map)
  8. Repository query methods. List (select new list)
  9. Entity Manager. Object Array
  10. Entity Manager. Tuple
  11. Entity Manager. Class-based Projections (select new)
  12. Entity Manager. Map (select new map)
  13. Entity Manager. List (select new list)
  14. Criteria API. Object Array
  15. Criteria API. Tuple
  16. Criteria API. Class-based Projections
  17. Criteria API. List

To one

Тестовый класс в котором можно увидеть все тесты с комментариями - ToOneTest.

Рассматриваются следующие кейсы:

  1. Repository derived methods. Interface-based flat projections
  2. Repository derived methods. Interface-based nested projections. Это как раз тот случай когда запрос выполняется, но работает не правильно, т.е. загружает больше полей чем надо.
  3. Repository derived methods. Class-based flat projections
  4. Repository query methods. Interface-based flat projections
  5. Repository query methods. Interface-based with nested dto class projections.
  6. Repository query methods. Class-based flat projections
  7. Repository query methods. Class-based nested class projections
  8. Repository query methods. Class-based nested map projections
  9. Repository query methods. Tuple
  10. Repository query methods. Map (select new map)
  11. Repository query methods. Object Array
  12. Repository query methods. List (select new list)
  13. Entity Manager. Class-based Projections (select new)
  14. Entity Manager. Tuple
  15. Entity Manager. Map (select new map)
  16. Entity Manager. Object Array
  17. Entity Manager. List (select new list)
  18. Criteria API. Tuple
  19. Criteria API. Class-based Projections
  20. Criteria API. Object Array
  21. Criteria API. List (list)

To many

Тестовый класс в котором можно увидеть все тесты с комментариями - ToManyTest.

Рассматриваются следующие кейсы:

  1. Repository derived methods. Interface-based projections
  2. Repository derived methods. Class-based projections
  3. Repository query methods. Interface-based nested projections
  4. Repository query methods. Class-based flat projections
  5. Repository query methods. Class-based nested projections
  6. Repository query methods. Object Array
  7. Repository query methods. Tuple
  8. Repository query methods. Map (select new map)
  9. Repository query methods. List (select new list)
  10. Entity Manager. Class-based Projections (select new)
  11. Entity Manager. Tuple
  12. Entity Manager. Map (select new map)
  13. Entity Manager. Object Array
  14. Entity Manager. List (select new list)
  15. Criteria API. Tuple
  16. Criteria API. Object Array
  17. Criteria API. Class-based Projections
  18. Criteria API. List (select new list)

Выводы

  1. Если мы пишем HQL/JPQL query, то мы контролируем запрос и возвращаем только то что мы хотим. Вопрос только в том как мапить.
  2. Если мы пишем HQL/JPQL всегда можно вернуть tuple или Map и помапить с него на dto.
  3. Использовать ли repository derived method, решает каждый сам. В простых случаях и HQL будет простым, в сложных длинна имени метода будет стремиться выйти за приделы нашей солнечной системы. В рамках решаемых задач для этого проекта, конкретно частичная выгрузка данных, repository derived method выглядит самым не безопасным. Мы не контролируем запрос, HQL может поменяться из-за изменения DTO/Projection или самой Entity.
  4. Когда мы работаем с Tuple очень удобно подключить библиотеку hibernate-jpamodelgen и использовать автогенеренные константы. В последней документации hibernate данный способ используется во всех примерах, можно сказать, что это тихая рекомендация. Также, используя эти константы легко создавать jakarta.persistence.criteria.Path для Criteria API.
  5. Когда мы пишем Query в spring data и используем DTO, по дефолту будут валидироваться выражение с DTO, будут проверены как типы, так и количество аргументов в конструкторе. А самое главное ни какой прокси магии.
  6. Не знаете что вернуть, верните Tuple. Это очень удобно.
  7. HQL + Class-Based Projection, он же DTO, он же select new class конструкция работают всегда.
  8. Для ToMany, при выгрузки данных в виде Projection/DTO/Tuple нам придется решать вопрос о мердже дублирующихся данных.

Built With

  • Spring Boot
  • Java 21
  • Spring Data JPA
  • Hibernate
  • H2
  • PostgreSQL
  • Junit

Getting Started

  1. Clone project
  2. Go to project directory
  3. Run project tests
./gradlew test

TODO

  • Надо ли рассматривать 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"?

About

All methods of partial loading of JPA entities

Topics

Resources

Stars

Watchers

Forks

Releases

No releases published

Packages

No packages published

Languages