Статьи

JPA 2 |

Вступление

Недавно я работал с 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 работает в целом и что ожидать, когда мы их используем.

Я надеюсь, вам понравилось сообщение в блоге. Если вы хотите продолжать читать интересные посты, вы можете следить за моим блогом.