Entity Graph
JPA Entity Graph задаёт план загрузки связей для конкретной операции. Он позволяет не менять глобальные LAZY/EAGER настройки сущности, когда разным use case нужен разный объём данных.
Important
Entity Graph описывает, что требуется загрузить, но не гарантирует один SQL-запрос. Провайдер может применить join, дополнительный select или batch. Сгенерированный SQL всегда проверяют.
Модель
@Entity
class Order {
@Id
private Long id;
@ManyToOne(fetch = FetchType.LAZY)
private Customer customer;
@OneToMany(mappedBy = "order", fetch = FetchType.LAZY)
private List<OrderLine> lines;
}Для списка заказов достаточно customer, а для карточки могут понадобиться customer и lines. Делать обе связи EAGER глобально — значит переплачивать во всех запросах.
Именованный граф
@NamedEntityGraph(
name = "order.card",
attributeNodes = {
@NamedAttributeNode("customer"),
@NamedAttributeNode(value = "lines", subgraph = "lines.product")
},
subgraphs = @NamedSubgraph(
name = "lines.product",
attributeNodes = @NamedAttributeNode("product")
)
)
@Entity
class Order {
// ...
}Map<String, Object> hints = Map.of(
"jakarta.persistence.fetchgraph",
entityManager.getEntityGraph("order.card")
);
Order order = entityManager.find(Order.class, id, hints);fetchgraph и loadgraph
fetchgraph: перечисленные атрибуты рассматриваются как eager для операции, неуказанные — как lazy, насколько это допускает провайдер и обязательные поля;loadgraph: перечисленные атрибуты добавляются к fetch-плану, а неуказанные сохраняют настройки mapping.
Если задача — жёстко ограничить план конкретным набором, начинают с fetchgraph. Но особенности Hibernate и фактический SQL всё равно проверяют тестом.
Динамический граф
EntityGraph<Order> graph = entityManager.createEntityGraph(Order.class);
graph.addAttributeNodes("customer");
Subgraph<OrderLine> lines = graph.addSubgraph("lines");
lines.addAttributeNodes("product");
List<Order> orders = entityManager.createQuery(
"select o from Order o where o.status = :status", Order.class)
.setParameter("status", status)
.setHint("jakarta.persistence.loadgraph", graph)
.getResultList();Динамический вариант полезен, когда план строится рядом с запросом и не нужен повторно.
Spring Data JPA
interface OrderRepository extends JpaRepository<Order, Long> {
@EntityGraph(attributePaths = {"customer", "lines"})
Optional<Order> findDetailedById(Long id);
}Имя метода не делает запрос «особенным» само по себе: @EntityGraph добавляет fetch-план к создаваемому запросу.
Ограничения
- загрузка коллекции вместе с пагинацией может привести к пагинации в памяти или неполному результату;
- несколько to-many связей дают декартово умножение строк;
- два bag-поля Hibernate иногда вообще отказывается fetch-join-ить одновременно;
- граф не заменяет DTO-проекцию, если нужны несколько полей из большой модели;
- entity graph не лечит N+1 автоматически — результат зависит от запроса и провайдера.
Для страницы списка часто лучше двухэтапная схема: сначала получить ids с пагинацией, затем загрузить нужный граф по ids.
На собеседовании
Entity Graph меняет mapping сущности? Нет, это fetch-план конкретной операции.
Будет ли один SQL? Не обязательно. Граф задаёт семантику загрузки, стратегию исполнения выбирает провайдер.
Чем отличается от join fetch? join fetch явно зафиксирован в JPQL; entity graph отделяет план загрузки от текста запроса и может переиспользоваться.
Связанные заметки: Fetch type Hibernate, Проблема N plus 1 Select Hibernate, Проблема декартова произведения Cartesian product problem в Hibernate, Spring Data JPA.