Статьи

5 способов инициализировать ленивые отношения и когда их использовать

Ленивое нагружение отношений между сущностями является хорошо известной наилучшей практикой в ​​JPA Его главная цель — извлечь из базы данных только запрошенные объекты и загрузить связанные объекты только при необходимости. Это отличный подход, если нам нужны только запрашиваемые объекты. Но это создает дополнительную работу и может стать причиной проблем с производительностью, если нам также понадобятся некоторые связанные объекты.

Давайте посмотрим на различные способы запуска инициализации и их конкретные преимущества и недостатки.

5

1. Вызовите метод сопоставленного отношения

Давайте начнем с самого очевидного и, к сожалению, самого неэффективного подхода. Мы используем метод find в EntityManager и вызываем метод для отношения.

1
2
Order order = this.em.find(Order.class, orderId);
order.getItems().size();

Этот код прекрасно работает, легко читается и часто используется. Так в чем же проблема?

Ну, вы, наверное, знаете это. Этот код выполняет дополнительный запрос для инициализации отношения. Это не похоже на настоящую проблему, но давайте посчитаем количество выполненных запросов в более реалистичном сценарии.

Допустим, у нас есть сущность с 5 отношениями, которую нам нужно инициализировать. Таким образом, мы получим 1 + 5 = 6 запросов . ОК, это 5 дополнительных запросов. Это все еще не кажется огромной проблемой.

Но наше приложение будет использоваться более чем одним пользователем параллельно (я надеюсь). Допустим, наша система должна обслуживать 100 параллельных пользователей. Тогда мы получим 100 + 5 * 100 = 600 запросов .

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

2. Получить Присоединиться в JPQL

Лучшим вариантом для инициализации отложенных отношений является использование запроса JPQL с выборочным соединением.

1
2
3
Query q = this.em.createQuery("SELECT o FROM Order o JOIN FETCH o.items i WHERE o.id = :id");
q.setParameter("id", orderId);
newOrder = (Order) q.getSingleResult();

Это говорит менеджеру сущности выбрать выбранную сущность и отношение в одном запросе. Преимущества и недостатки этого подхода очевидны:

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

И главный недостаток в том, что нам нужно написать дополнительный код, который выполняет запрос. Но это становится еще хуже, если у сущности есть несколько отношений, и нам нужно инициализировать разные отношения для разных вариантов использования. В этом случае нам нужно написать запрос для каждой необходимой комбинации выбранных отношений. Это может стать довольно грязным.

Использование выборочных объединений в операторах JPQL может потребовать большого количества запросов, что усложнит поддержку кодовой базы. Поэтому, прежде чем мы начнем писать много запросов, мы должны подумать о количестве различных комбинаций соединения выборки, которые могут нам понадобиться. Если число мало, то это хороший способ ограничить количество выполняемых запросов.

3. Получить Присоединиться в Criteria API

Хорошо, этот подход в основном такой же, как и раньше. Но на этот раз мы используем Criteria API вместо JPQL-запроса.

1
2
3
4
5
6
7
8
CriteriaBuilder cb = em.getCriteriaBuilder();
CriteriaQuery q = cb.createQuery(Order.class);
Root o = q.from(Order.class);
o.fetch("items", JoinType.INNER);
q.select(o);
q.where(cb.equal(o.get("id"), orderId));
 
Order order = (Order)this.em.createQuery(q).getSingleResult();

Преимущества и недостатки те же, что и для запроса JPQL с выборочным соединением. Сущность и отношение извлекаются одним запросом из базы данных, и нам нужен конкретный код для каждой комбинации отношений. Но у нас часто уже есть много кода запроса для конкретных случаев, если мы используем Criteria API. Так что это не может быть большой проблемой.

Если мы уже используем Criteria API для построения запроса, это хороший подход для уменьшения количества выполненных запросов.

4. График именованных объектов

Графики именованных объектов являются новой функцией JPA 2.1. Его можно использовать для определения графа сущностей, который должен запрашиваться из базы данных. Определение графа сущностей выполняется с помощью аннотаций и не зависит от запроса.

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

1
2
3
4
5
@Entity
@NamedEntityGraph(name = "graph.Order.items",
      attributeNodes = @NamedAttributeNode("items"))
public class Order implements Serializable {
....

Именованный граф сущностей может затем использоваться методом find объекта EntityManager.

1
2
3
4
5
6
EntityGraph graph = this.em.getEntityGraph("graph.Order.items");
   
Map hints = new HashMap();
hints.put("javax.persistence.fetchgraph", graph);
   
Order order = this.em.find(Order.class, orderId, hints);

Это в основном улучшенная версия нашего первого подхода. Менеджер сущностей извлечет определенный граф сущностей из базы данных одним запросом. Единственным недостатком является то, что нам необходимо аннотировать именованный граф сущностей для каждой комбинации отношений, которые должны быть получены в рамках одного запроса. Нам потребуется меньше дополнительных аннотаций, как при втором подходе, но он все еще может стать довольно грязным.

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

5. Динамический Entity Graph

Динамический граф сущностей подобен графу именованных сущностей и также объяснялся в одном из предыдущих постов . Единственное отличие состоит в том, что граф сущностей определяется через Java API.

1
2
3
4
5
6
7
EntityGraph graph = this.em.createEntityGraph(Order.class);
Subgraph itemGraph = graph.addSubgraph("items");
     
Map hints = new HashMap();
hints.put("javax.persistence.loadgraph", graph);
   
Order order = this.em.find(Order.class, orderId, hints);

Определение через API может быть преимуществом и недостатком. Если нам нужно много графов сущностей, зависящих от конкретного случая использования, может быть лучше определить граф сущности в конкретном коде Java и не добавлять дополнительную аннотацию к сущности. Это позволяет избежать объектов с десятками аннотаций. С другой стороны, динамический граф сущностей требует больше кода и дополнительного метода для повторного использования.

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

Вывод

Мы рассмотрели 5 различных способов инициализации ленивых отношений. И, как мы видели, у каждого из них есть свои преимущества и недостатки. Так что же помнить из этой статьи?

  • Инициализация отложенного отношения посредством вызова метода в отображенном отношении вызывает дополнительный запрос. Этого следует избегать по соображениям производительности.
  • Соединения извлечения в операторах JPQL сокращают количество запросов до одного, но нам может понадобиться много разных запросов.
  • Criteria API также поддерживает выборочные соединения, и нам нужен специальный код для каждой комбинации отношений, которые должны быть инициализированы.
  • Графики именованных сущностей являются хорошим решением, если мы будем использовать определенный граф в нашем коде.
  • Динамические графы сущностей могут быть лучшим решением, если нам нужно определить граф для конкретного случая использования.