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.