Hibernate: инициализация сложного объекта

у меня проблемы с полной загрузкой очень сложного объекта из БД в разумные сроки и с разумным количеством запросов.

мой объект имеет много внедренных сущностей, каждая сущность имеет ссылки на другие сущности, другие сущности ссылаются еще на одну и так далее (Итак, уровень вложенности 6)

Итак, я создал пример, чтобы продемонстрировать, что я хочу: https://github.com/gladorange/hibernate-lazy-loading

Я Пользователь.

пользователь @OneToMany сборники любимых апельсины,яблоки,виноград и персики. Каждая виноградная лоза имеет @OneToMany сбор винограда. Каждый плод-это еще одна сущность с одним строковым полем.

Я создаю пользователя с 30 любимыми фруктами каждого типа, и каждая виноградная лоза имеет 10 виноградин. Итак, всего у меня есть 421 сущность в DB - 30*4 фрукты, 100*30 виноград и один пользователь.

и что я хочу: я хочу загрузить их, используя не более 6 SQL запросы. И каждый запрос не должен создавать большой результирующий набор (big-это результирующий набор с более чем 200 записями для этого примера).

мое идеальное решение будет следующим:

  • 6 просит. Первый запрос возвращает информацию о пользователе, а размер результирующего набора равен 1.

  • второй запрос возвращает информацию о яблоках для этого пользователя, а размер результирующего набора равен 30.

  • третий, четвертый и пятый запросы возвращает то же самое, что и второе (с размером результирующего набора = 30), но для виноградных лоз, апельсинов и персиков.

  • шестой запрос возвращает виноград для всех виноградных лоз

Это очень просто в мире SQL, но я не могу достичь такого с JPA (Hibernate).

Я пробовал следующие подходы:

  1. используйте fetch join, например from User u join fetch u.oranges .... Это ужасно. Результирующий набор 30*30*30*30 и время выполнения-10 секунд. Количество запросы = 3. Я пробовал без винограда, с виноградом вы получите х10 размер результирующего набора.

  2. просто используйте ленивую загрузку. Это лучший результат в этом примере (с @Fetch= SUBSELECT для винограда). Но в этом случае мне нужно вручную перебирать каждую коллекцию элементов. Кроме того, subselect fetch-слишком глобальная настройка, поэтому я хотел бы иметь что-то, что может работать на уровне запроса. Результирующий набор и время близки к идеалу. 6 запросы и 43 ms.

  3. загрузка с помощью entity graph. То же самое, что и fetch join, но он также делает запрос на каждый виноград, чтобы получить его виноград. Однако, время результат лучше (6 секунд), но все равно ужасно. Количество запросов > 30.

  4. Я попытался обмануть JPA с "ручной" загрузкой сущностей в отдельном запросе. Например:

    SELECT u FROM User where id=1;
    SELECT a FROM Apple where a.user_id=1;
    

Это немного хуже, чем ленивая загрузка, поскольку для каждой коллекции требуется два запроса: первый запрос на ручную загрузку сущностей (я полностью контролирую этот запрос, включая загрузку связанных сущностей), второй запрос на ленивую загрузку тех же сущностей самим Hibernate (это выполняется автоматически Hibernate)

время выполнения 52, количество запросов = 10 (1 для пользователя, 1 для винограда, 4*2 для каждой коллекции фруктов)

на самом деле," ручное "решение в сочетании с SUBSELECT fetch позволяет мне использовать" простые " соединения выборки для загрузки необходимых объектов в один запрос (например,@OneToOne объекты), поэтому я собираюсь использовать его. Но мне не нравится, что мне нужно выполнить два запроса для загрузки коллекции.

какие предложения?

3 ответов


Я собираюсь предложить еще один вариант о том, как лениво собирать коллекции винограда в винограднике:

@OneToMany
@BatchSize(size = 30)
private List<Grape> grapes = new ArrayList<>();

вместо того, чтобы делать под-выбор, этот будет использовать in (?, ?, etc) чтобы получить много коллекций Grapes Сразу. Вместо ? будут переданы идентификаторы Grapevine. Это противоречит запросу 1 List<Grape> коллекция за раз.

Это еще одна техника в вашем арсенале.


Я обычно покрываю 99% таких случаев использования с помощью пакетная выборка по обоим объектам и коллекциям. Если вы обрабатываете извлеченные объекты в той же транзакции / сеансе, в которой вы их читаете, то вам больше ничего не нужно делать, просто перейдите к ассоциациям, необходимым логике обработки, и сгенерированные запросы будут очень оптимальными. Если вы хотите вернуть извлеченные объекты как отсоединенные, инициализируйте ассоциации вручную:

User user = entityManager.find(User.class, userId);
Hibernate.initialize(user.getOranges());
Hibernate.initialize(user.getApples());
Hibernate.initialize(user.getGrapevines());
Hibernate.initialize(user.getPeaches());
user.getGrapevines().forEach(grapevine -> Hibernate.initialize(grapevine.getGrapes()));

обратите внимание, что последняя команда не на самом деле выполнить запрос для каждой виноградной лозы, как несколько grapes коллекции (до указанного @BatchSize) инициализируются при инициализации первого. Вы просто повторяете все из них, чтобы убедиться, что все инициализированы.

этот метод напоминает ваш ручной подход, но более эффективен (запросы не повторяются для каждой коллекции), и более удобочитаем и обслуживаем в моем мнение (вы просто позвоните Hibernate.initialize вместо того, чтобы вручную писать тот же запрос, который Hibernate генерирует автоматически).


Я не совсем понимаю ваши требования здесь. Мне кажется, вы хотите, чтобы Hibernate сделал что-то, что он не предназначен для этого, и когда он не может, вы хотите взломать решение, которое далеко не оптимально. Почему бы не ослабить ограничения и не получить то, что работает? Почему у вас вообще есть эти ограничения?

некоторые общие советы:

  1. при использовании Hibernate / JPA вы не контролируете запросы. Вы тоже не должны (с несколькими исключения.) Сколько запросов, в каком порядке они выполняются и т. д., В значительной степени вне вашего контроля. Если вы хотите полностью контролировать свои запросы, просто пропустите JPA и используйте JDBC вместо этого (Spring JDBC, например.)
  2. понимание ленивой загрузки является ключом к принятию решений в таких ситуациях. Лениво загруженные отношения не извлекается при получении объекта-владельца, вместо этого Hibernate возвращается в базу данных и получает их, когда они фактически используются. Какие средства эта ленивая загрузка окупается, если вы не используете атрибут каждый раз, но имеете штраф за время его использования. (Fetch join используется для нетерпеливого извлечения ленивого отношения. На самом деле не предназначен для использования с регулярной загрузкой из базы данных.)
  3. оптимизация запросов с помощью Hibernate не должны быть вашей первой линией действий. Всегда начинайте с базы данных. Правильно ли он смоделирован, с первичными ключами и внешними ключами, обычными формами и т. д.? У вас есть поисковые индексы в соответствующих местах (обычно на внешних ключах)?
  4. тестирование производительности на очень ограниченном наборе данных, вероятно, не даст лучших результатов. Вероятно, будут накладные расходы с подключениями и т. д., которые будут больше времени, потраченного на фактическое выполнение запросов. Кроме того, могут быть случайные сбои, которые стоят несколько миллисекунд, что даст результат, который может ввести в заблуждение.
  5. небольшой совет от просмотра вашего кода: никогда не предоставляйте сеттеры для коллекций в сущностях. Если на самом деле вызванный в транзакции, Hibernate выдаст исключение.
  6. tryManualLoading, вероятно, делает больше, чем вы думаете. Сначала он извлекает пользователя (с ленивой загрузкой), затем он извлекает каждый из фруктов, затем он снова извлекает фрукты через ленивую загрузку. (Если Hibernate не понимает, что запросы будут такими же, как при ленивой загрузке.)
  7. на самом деле вам не нужно перебирать всю коллекцию, чтобы инициировать ленивую загрузку. Вы можете сделать это user.getOranges().size(), или Hibernate.initialize(user.getOranges()). Для виноградной лозы вам придется повторить, чтобы инициализировать все виноградины.

при правильном дизайне базы данных и ленивой загрузке в правильных местах не должно быть необходимости ни в чем, кроме:

em.find(User.class, userId);

а затем, возможно, запрос выборки соединения, если ленивая загрузка занимает много времени.

по моему опыту, самым важным фактором для ускорения спящего режима является поиск индексов в базе данных.