JPA Entity Graph и нюансы его использования

Entity Graph — это один из мощных инструментов JPA, который помогает разработчикам гибко управлять загрузкой связанных сущностей. Entity Graph позволяет динамически настраивать загрузку данных во время выполнения программы, что делает его особенно полезным в проектах с сложными структурами данных.

Команда Spring АйО подготовила статью, в которой рассмотрела, как использовать Entity Graph.

В версии JPA 2.1 была добавлена функция Entity Graph, которая улучшает производительность загрузки связанных сущностей. Ранее, в JPA 2.0, для управления стратегиями загрузки данных использовались два подхода: FetchType.LAZY и FetchType.EAGER. Однако эти стратегии статичны, что не позволяет гибко переключаться между ними во время выполнения программы. 

Конечно, существует join fetch, который позволяет превратить ленивую загрузку (lazy) в жадную (eager) со стратегией JOIN. Но обратного способа — чтобы из жадной сделать ленивую — средствами JPQL не предусмотрено. И тут на помощь приходит @EntityGraph! Давайте разберёмся, как с ним работать более подробно…

Прежде чем мы приступим к рассмотрению Entity Graph, определим предметную область, с которой собираемся работать. Допустим, мы хотим создать сайт-блог, где пользователи могут комментировать посты и делиться ими.

Итак, для начала объявим сущность User:

@Entity
public class User {
  
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;

    private String name;

    private String email;
}

Пользователь может делиться различными публикациями, поэтому нам также нужна сущность Post:

@Entity
public class Post {

    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;

    private String subject;

    @OneToMany(mappedBy = "post", fetch = FetchType.LAZY)
    private List comments = new ArrayList<>();

    @ManyToOne(fetch = FetchType.EAGER)
    @JoinColumn(name = "user_id")
    private User user;
}

Пользователь также может комментировать опубликованные сообщения, поэтому добавим сущность Comment:

@Entity
public class Comment {

    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;

    private String reply;

    @ManyToOne(fetch = FetchType.EAGER)
    @JoinColumn
    private Post post;

    @ManyToOne(fetch = FetchType.EAGER)
    @JoinColumn
    private User user;
}

Сущность Post имеет связь с сущностями Comment и User. Сущность Comment имеет связь с сущностями Post и User.

Наша задача состоит в том, чтобы загрузить граф:

Post  ->  user:User
      ->  comments:List
            comments[0]:Comment -> user:User
            comments[1]:Comment -> user:User

До появления Entity Graph разработчики использовали стратегии FetchType для управления загрузкой связанных данных:

FetchType.EAGER: Сущность загружается сразу вместе с ее связями (но не всегда именно одним запросом). Подход используется по умолчанию для аннотаций @ManyToOne и @OneToOne.

FetchType.LAZY: Связанные данные загружаются только тогда, когда они запрашиваются. Это поведение по умолчанию для связей @OneToMany, @ManyToMany и @ElementCollection.

Например, при использовании LAZY для сущности Post, комментарии к посту (сущность Comment) не будут загружаться по умолчанию:

@OneToMany(mappedBy = "post", fetch = FetchType.LAZY)
private List comments = new ArrayList<>();

В то же время для связи ManyToOne поведение по умолчанию — EAGER, что приводит к автоматической загрузке связанных данных:

@ManyToOne
@JoinColumn(name = "user_id")
private User user;

Однако, чтобы сделать загрузку более гибкой и управляемой на уровне выполнения, и используется Entity Graph.

Рассмотрим пример с использование @EntityGraph и Spring Data:

@EntityGraph(attributePaths = {"comments"})
List findEntityGraphTypeFetchBySubject(String subject);

Граф будет включать в себя загрузку всех базовых полей пользователя, который создал пост (user), а также комментарии и их авторов.

Hibernate: 
    select
        p1_0.id,
        c1_0.post_id,
        c1_0.id,
        c1_0.reply,
        c1_0.user_id,
        p1_0.subject,
        p1_0.user_id 
    from
        post p1_0 
    left join
        comment c1_0 
            on p1_0.id=c1_0.post_id 
    where
        p1_0.subject=?

Стоит отметить, что у самого EntityGraph также есть 2 вида загрузки: EntityGraph.EntityGraphType.FETCH  и EntityGraph.EntityGraphType.LOAD. При выборе режима Fetch (используется по умолчанию) ассоциативные атрибуты, явно объявленные для загрузки, например comments в нашем случае, будут выгружены жадно (FetchType.EAGER), остальные же атрибуты будут загружены лениво (FetchType.LAZY).

В случае же если мы будем использовать режим EntityGraph.EntityGraphType.LOAD, выбранныеатрибуты будут загружены жадно, а остальные атрибуты будут загружены в соответствии с тем, какой FetchType указан в модели.

@EntityGraph(attributePaths = {"comments"}, type = EntityGraph.EntityGraphType.LOAD)
List findEntityGraphTypeLoadBySubject(String subject);
Hibernate: 
    select
        p1_0.id,
        c1_0.post_id,
        c1_0.id,
        c1_0.reply,
        c1_0.user_id,
        p1_0.subject,
        p1_0.user_id 
    from
        post p1_0 
    left join
        comment c1_0 
            on p1_0.id=c1_0.post_id 
    where
        p1_0.subject=?
Hibernate: 
    select
        u1_0.id,
        u1_0.email,
        u1_0.name 
    from
        user_ u1_0 
    where
        u1_0.id=?
Hibernate: 
    select
        u1_0.id,
        u1_0.email,
        u1_0.name 
    from
        user_ u1_0 
    where
        u1_0.id=?

//И так далее, N+1 запрос

Как можно заметить, вся информация о пользователях грузится в соответствии с тем, как указано в модели — то есть жадно, хоть мы и не указывали поле user для @EntityGraph.

Отдельно отметим, базовые атрибуты будут загружены всегда, независимо от выбранного режима загрузки, будь то FETCH или LOAD. Поэтому указывать их в качестве значений для attributePaths нет смысла.

2fac6a07463c37d88e1330dbe26a2b68.png

Присоединяйтесь к русскоязычному сообществу разработчиков на Spring Boot в телеграм — Spring АйО, чтобы быть в курсе последних новостей из мира разработки на Spring Boot и всего, что с ним связано.

Ждем всех,  присоединяйтесь

© Habrahabr.ru