Вступление
Недавно я работал с FETCH JOINS в JPA 2, чтобы охотно получать данные из базы данных, и я довольно много узнал о том, почему мы должны избегать использования Fetch Joins в наших повседневных операциях.
Сегодняшнее сообщение в блоге рассказывает о моем опыте работы с Fetch и моем обучении (в основном на основе комментариев, которые я получил, когда в моем запросе было много FETCH JOINS)
Постановка задачи
В нашем проекте была ситуация, когда сущность выбиралась из базы данных, в которой было определено много ассоциаций с ценным набором (OneToMany, ManyToMany, также называемая ассоциацией ToMany). Вот концептуальная картина того, как выглядел объект (для ясности опущены геттеры и сеттеры). Это чрезвычайно упрощенный пример сущности. В реальном сценарии у нас было около 11 ассоциаций.
01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
|
public class ItemRecord { @Id private String itemId; @Column (name= "author" ) private String author; @OneToMany (cascade = CascadeType.ALL, mappedBy = "item" ) private List costs; @OneToMany (cascade = CascadeType.ALL, mappedBy = "item" ) private List notes; @OneToMany (cascade = CascadeType.ALL, mappedBy = "item" ) private List stats; } |
Есть несколько вещей, на которые следует обратить внимание в отношении вышеупомянутой сущности:
- Он имеет 3 коллекционные ассоциации.
- Все эти ассоциации лениво выбираются, поскольку стратегией выборки по умолчанию для Коллекции с ценным объединением в JPA является Lazy.
В нашей бизнес-реализации у нас есть переводчик, который берет значение, возвращаемое нашим уровнем DAO, и преобразует его в Business DTO.
Алгоритм нашего бизнес-метода был таким:
1
2
3
4
5
6
7
|
@TransactionAttribute public List searchItemRecords (SearchCriteria sc) { List ir = itemRecordDao.search(sc); List convertedData = recordConverter.convert(ir); return convertedData; } |
Обратите внимание, что весь метод выполняется внутри транзакции.
Всякий раз, когда мы выбирали данные из базы данных, ни одна из связанных данных о стоимости, статистике и т. Д. Не была получена с нетерпением. Но наша ItemInformation DTO ожидала всех данных. Поэтому, когда getCosts или getStatistics были вызваны в первый раз, провайдер персистентности (в нашем случае Hibernate) отправил запрос в базу данных для получения указанных данных. Это создавало проблему запроса N + 1 Select для нас. Если вы не знакомы с N + 1 Selects или нуждаетесь в обновлении, вы можете посмотреть эту статью на DZone .
Решение
Большинство из нас, включая меня, выбрали бы самое быстрое и простое решение проблемы выбора N + 1, которое заключается в использовании Fetch Join. Это также часто предлагается в различных блогах / статьях, распространяемых в Интернете.
Поэтому я тоже придерживался того же подхода. И это был ПЛОХОЙ подход, в моем случае, по крайней мере.
Давайте сначала посмотрим, как мы можем использовать FETCH JOIN.
Запрос перед использованием Fetch Join был следующим:
1
|
SELECT item FROM ItemRecord item WHERE author=:author; |
Обратите внимание, что запрос в форме JPA.
Этот запрос не получил значения коллекции. В результате в переводчике, когда мы делали getCosts для каждого ItemRecord, был выполнен запрос, подобный приведенному ниже:
1
|
SELECT cost FROM Cost where itemId = :itemId |
Таким образом, если бы у нас было 3 ItemRecords, то общее количество запросов SELECT, запущенных к базе данных:
- 1 для получения всех ItemRecords
- 3 для получения затрат для каждого ItemRecord
- 3 для получения заметок для каждого ItemRecord
- 3 для получения статистики для каждой ItemRecord
т.е. (3 раза 3) + 1
который при обобщении переводит в N раз М + 1,
где,
- N — количество найденных записей первичного объекта
- M — количество ассоциативно-ценных ассоциаций в первичной сущности
- 1 запрос для получения всех первичных объектов
В нашем реальном сценарии у нас было 11 ассоциаций. Таким образом, для каждого первичного объекта ItemRecord мы запускали 11 запросов SELECT. Количество выполненных запросов умножается для каждой найденной ItemRecord.
Создание ассоциаций с ценным набором EAGER не было возможным, так как в Entity выполнялось много других запросов, которые требовали только выбранные данные.
Это не оптимальное решение. С этим нужно было что-то делать, и многие интернет-статьи предлагали использовать FETCH JOINS, поскольку их было проще всего реализовать и решить проблему запроса (N Time M + 1).
Поэтому я решил использовать соединения FETCH в своем запросе, чтобы охотно получить все данные для данного сценария.
Запрос выглядел примерно так:
1
2
3
4
5
|
SELECT item FROM ItemRecord JOIN FETCH item.costs, JOIN FETCH item.notes, JOIN FETCHitem.stats; |
Барьер 1
Я довольно быстро понял, что этот запрос не будет работать в моем сценарии, так как у меня может быть ItemRecord, который не имеет никакой статистики, связанной с ним, и в таком случае вышеупомянутый запрос не вернет мне этот ItemRecord (из-за того, как работает ORM) и поэтому у меня будут неполные данные.
Итак, я затем перешел к LEFT JOIN FETCH, который будет возвращать мне сущности ItemRecord, даже если некоторые из связанных отношений пусты. Запрос выглядел так:
1
2
3
4
5
|
SELECT item FROM ItemRecord LEFT JOIN FETCH item.costs, LEFT JOIN FETCH item.notes, LEFT JOIN FETCH item.stats; |
Барьер 2
Когда я запустил свои модульные тесты для проверки вышеуказанного запроса, я получил исключение:
1
|
javax.persistence.PersistenceException: org.hibernate.HibernateException: cannot simultaneously fetch multiple bags |
В чем проблема?
Проблема в том, что я использую список в качестве типа коллекции в моей сущности. Использование List приводит в замешательство JPA / Hibernate. Эта путаница хорошо документирована в этой статье .
Чтобы обойти эту проблему, я решил использовать Set вместо List, главным образом потому, что это было самое простое из трех решений, представленных в вышеприведенном сообщении в блоге, и оно также имело смысл (по крайней мере, во время реализации).
Таким образом, я изменил свою сущность так, чтобы она была установлена как коллекция вместо списка, и измененная сущность выглядела так:
01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
|
public class ItemRecord { @Id private String itemId; @Column (name= "author" ) private String author; @OneToMany (cascade = CascadeType.ALL, mappedBy = "item" ) private Set costs; @OneToMany (cascade = CascadeType.ALL, mappedBy = "item" ) private Set notes; @OneToMany (cascade = CascadeType.ALL, mappedBy = "item" ) private Set stats; } |
Я снова запустил свои тестовые примеры, и мой тестовый пример, который проверял запрос, был успешным. Yippie. НО, мой другой контрольный пример, который проверял записи в разделе заметок, не удался. Глядя на контрольный пример, я понял, что мне нужны данные в определенном порядке, в том порядке, в котором они были введены, и я использовал HashSet, который не упорядочен. Решение было простым. Используйте LinkedHashSet, который поддерживает порядок элементов.
Использование LinkedHashSet помогло, и мой тестовый пример пройден.
Я был очень счастлив, но мое счастье было недолгим.
Барьер 3
У меня был другой тестовый пример, который ожидал 3 объекта стоимости для данного ItemRecord. Как только я перешел к реализации Set, тест начал проваливаться. Оказалось, что у меня был неправильный hashCode и соответствует реализации для моего объекта Entity, который возвращал один и тот же хэш-код для двух разных объектов, и в результате только одна сущность когда-либо сохранялась, так как Set не допускает дублирования значений.
Итак, следующая вещь, которую я должен был сделать, — иметь правильную реализацию HashCode и Equals для всех моих сущностей.
Финальный барьер
Наконец, когда все мои тестовые примеры начали проходить, я сделал обзор кода и отправил его команде.
Первым, кого это волновало, был мой технический руководитель. 🙂
Он был просто в ярости, чтобы посмотреть на FETCH JOINS. Причина? Причина заключалась в том, что соединения LEFT FETCH возвращают декартово произведение ВСЕХ данных. При таком количестве данных, которое у нас есть, стало бы кошмаром даже поддерживать множественный выбор в ItemRecord. Вся проблема может быть легко понята из этого поста в блоге .
Таким образом, я пытался решить проблему с производительностью, и оказалось, что я на самом деле создавал проблему с большей производительностью. 🙂
Целостное решение перехода на FETCH JOIN было отброшено, и было решено дополнительно изучить, почему нам нужны все данные в пользовательском интерфейсе и почему мы не можем разделить выборку данных на более мелкие выделенные транзакции.
Резюме:
Весь процесс работы с Fetch Joins дал мне хорошее понимание того, как Joins работает в целом и что ожидать, когда мы их используем.
Я надеюсь, вам понравилось сообщение в блоге. Если вы хотите продолжать читать интересные посты, вы можете следить за моим блогом.