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 нет смысла.
Присоединяйтесь к русскоязычному сообществу разработчиков на Spring Boot в телеграм — Spring АйО, чтобы быть в курсе последних новостей из мира разработки на Spring Boot и всего, что с ним связано.
Ждем всех, присоединяйтесь