«1. Обзор

JPA 2.1 представила функцию Entity Graph как более сложный метод работы с нагрузкой на производительность.

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

В этом уроке мы более подробно объясним, как создать и использовать эту функцию.

2. Что пытается разрешить граф сущностей

До JPA 2.0 для загрузки ассоциации сущностей мы обычно использовали FetchType.LAZY и FetchType.EAGER в качестве стратегий выборки. Это указывает поставщику JPA дополнительно получать связанную ассоциацию или нет. К сожалению, эта мета-конфигурация является статической и не позволяет переключаться между этими двумя стратегиями во время выполнения.

Основной целью JPA Entity Graph является повышение производительности среды выполнения при загрузке связанных ассоциаций и основных полей сущности.

Короче говоря, провайдер JPA загружает весь граф в один запрос выбора, а затем избегает получения ассоциации с другими запросами SELECT. Это считается хорошим подходом для повышения производительности приложений.

3. Определение модели

Прежде чем мы начнем изучать 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")
    private List<Comment> comments = new ArrayList<>();

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

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

@Entity
public class Comment {

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

    private String reply;

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

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

Как мы видим, объект Post связан с объектами Comment и User. Объект Comment связан с объектами Post и User.

Цель состоит в том, чтобы загрузить следующий граф различными способами:

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

4. Загрузка связанных сущностей с помощью стратегий FetchType

Метод FetchType определяет две стратегии для выборки данных из базы данных:

    FetchType .EAGER: поставщик сохраняемости должен загрузить соответствующее аннотированное поле или свойство. Это поведение по умолчанию для аннотированных полей @Basic, @ManyToOne и @OneToOne. FetchType.LAZY: поставщик постоянства должен загружать данные при первом доступе к ним, но его можно загружать с нетерпением. Это поведение по умолчанию для полей с аннотациями @OneToMany, @ManyToMany и @ElementCollection.

Например, когда мы загружаем сущность Post, связанные сущности Comment не загружаются как тип FetchType по умолчанию, поскольку @OneToMany имеет значение LAZY. Мы можем переопределить это поведение, изменив FetchType на EAGER:

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

Для сравнения, когда мы загружаем объект Comment, его родительский объект Post загружается как режим по умолчанию для @ManyToOne, то есть EAGER. Мы также можем не загружать сущность Post, изменив эту аннотацию на LAZY:

@ManyToOne(fetch = FetchType.LAZY) 
@JoinColumn(name = "post_id") 
private Post post;

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

Теперь, поскольку мы использовали аннотации для описания нашей стратегии выборки, наше определение статично, и нет возможности переключаться между LAZY и EAGER во время выполнения.

Здесь в игру вступает Entity Graph, как мы увидим в следующем разделе.

5. Определение графа сущностей

Чтобы определить граф сущностей, мы можем либо использовать аннотации к сущности, либо действовать программно, используя JPA API.

5.1. Определение графа сущностей с помощью аннотаций

Аннотация @NamedEntityGraph позволяет указать атрибуты, которые следует включать, когда мы хотим загрузить сущность и связанные с ней ассоциации.

Итак, давайте сначала определим Entity Graph, который загружает сообщение и связанные с ним объекты User и Comment:

@NamedEntityGraph(
  name = "post-entity-graph",
  attributeNodes = {
    @NamedAttributeNode("subject"),
    @NamedAttributeNode("user"),
    @NamedAttributeNode("comments"),
  }
)
@Entity
public class Post {

    @OneToMany(mappedBy = "post")
    private List<Comment> comments = new ArrayList<>();
    
    //...
}

В этом примере мы использовали @NamedAttributeNode для определения связанных объектов, которые будут загружены, когда корень сущность загружена.

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

«Для этой цели мы будем использовать атрибут подграфа @NamedAttributeNode. Это позволяет ссылаться на именованный подграф, определенный с помощью аннотации @NamedSubgraph:

@NamedEntityGraph(
  name = "post-entity-graph-with-comment-users",
  attributeNodes = {
    @NamedAttributeNode("subject"),
    @NamedAttributeNode("user"),
    @NamedAttributeNode(value = "comments", subgraph = "comments-subgraph"),
  },
  subgraphs = {
    @NamedSubgraph(
      name = "comments-subgraph",
      attributeNodes = {
        @NamedAttributeNode("user")
      }
    )
  }
)
@Entity
public class Post {

    @OneToMany(mappedBy = "post")
    private List<Comment> comments = new ArrayList<>();
    //...
}

Определение аннотации @NamedSubgraph аналогично @NamedEntityGraph и позволяет указывать атрибуты связанной ассоциации. Таким образом, мы можем построить полный граф.

В приведенном выше примере с определенным графом «post-entity-graph-with-comment-users» мы можем загрузить сообщение, связанного с ним пользователя, комментарии и пользователей, связанных с комментариями.

Наконец, обратите внимание, что мы также можем добавить определение Entity Graph, используя дескриптор развертывания orm.xml:

<entity-mappings>
  <entity class="com.baeldung.jpa.entitygraph.Post" name="Post">
    ...
    <named-entity-graph name="post-entity-graph">
            <named-attribute-node name="comments" />
    </named-entity-graph>
  </entity>
  ...
</entity-mappings>

5.2. Определение Entity Graph с помощью JPA API

Мы также можем определить Entity Graph через EntityManager API, вызвав метод createEntityGraph():

EntityGraph<Post> entityGraph = entityManager.createEntityGraph(Post.class);

Чтобы указать атрибуты корневого объекта, мы используем addAttributeNodes( ) метод.

entityGraph.addAttributeNodes("subject");
entityGraph.addAttributeNodes("user");

Точно так же, чтобы включить атрибуты из связанной сущности, мы используем addSubgraph() для построения встроенного графа сущностей, а затем мы используем addAttributeNodes(), как мы делали выше.

entityGraph.addSubgraph("comments")
  .addAttributeNodes("user");

Теперь, когда мы увидели, как создать Entity Graph, мы рассмотрим, как его использовать в следующем разделе.

6. Использование Entity Graph

6.1. Типы графов сущностей

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

    javax.persistence.fetchgraph — только указанные атрибуты извлекаются из базу данных. Поскольку в этом руководстве мы используем Hibernate, мы можем заметить, что, в отличие от спецификаций JPA, атрибуты, статически настроенные как EAGER, также загружаются. javax.persistence.loadgraph — в дополнение к указанным атрибутам также извлекаются атрибуты, статически сконфигурированные как EAGER.

В любом случае первичный ключ и версия всегда загружаются.

6.2. Загрузка графа сущностей

Мы можем получить граф сущностей различными способами.

Начнем с метода EntityManager.find(). Как мы уже показали, режим по умолчанию основан на статических метастратегиях FetchType.EAGER и FetchType.LAZY.

Итак, давайте вызовем метод find() и просмотрим журнал:

Post post = entityManager.find(Post.class, 1L);

Вот журнал, предоставленный реализацией Hibernate:

select
    post0_.id as id1_1_0_,
    post0_.subject as subject2_1_0_,
    post0_.user_id as user_id3_1_0_ 
from
    Post post0_ 
where
    post0_.id=?

Как мы видим из журнала, объекты User и Comment не загружаются.

Мы можем переопределить это поведение по умолчанию, вызвав перегруженный метод find(), который принимает подсказки как карту. Затем мы можем указать тип графика, который мы хотим загрузить:

EntityGraph entityGraph = entityManager.getEntityGraph("post-entity-graph");
Map<String, Object> properties = new HashMap<>();
properties.put("javax.persistence.fetchgraph", entityGraph);
Post post = entityManager.find(Post.class, id, properties);

Если мы снова посмотрим в журнал, мы увидим, что эти объекты теперь загружены и только в одном запросе выбора:

select
    post0_.id as id1_1_0_,
    post0_.subject as subject2_1_0_,
    post0_.user_id as user_id3_1_0_,
    comments1_.post_id as post_id3_0_1_,
    comments1_.id as id1_0_1_,
    comments1_.id as id1_0_2_,
    comments1_.post_id as post_id3_0_2_,
    comments1_.reply as reply2_0_2_,
    comments1_.user_id as user_id4_0_2_,
    user2_.id as id1_2_3_,
    user2_.email as email2_2_3_,
    user2_.name as name3_2_3_ 
from
    Post post0_ 
left outer join
    Comment comments1_ 
        on post0_.id=comments1_.post_id 
left outer join
    User user2_ 
        on post0_.user_id=user2_.id 
where
    post0_.id=?

Давайте посмотрим, как мы можем добиться того же самого с помощью JPQL:

EntityGraph entityGraph = entityManager.getEntityGraph("post-entity-graph-with-comment-users");
Post post = entityManager.createQuery("select p from Post p where p.id = :id", Post.class)
  .setParameter("id", id)
  .setHint("javax.persistence.fetchgraph", entityGraph)
  .getSingleResult();

И, наконец, давайте посмотрим на пример Criteria API:

EntityGraph entityGraph = entityManager.getEntityGraph("post-entity-graph-with-comment-users");
CriteriaBuilder criteriaBuilder = entityManager.getCriteriaBuilder();
CriteriaQuery<Post> criteriaQuery = criteriaBuilder.createQuery(Post.class);
Root<Post> root = criteriaQuery.from(Post.class);
criteriaQuery.where(criteriaBuilder.equal(root.<Long>get("id"), id));
TypedQuery<Post> typedQuery = entityManager.createQuery(criteriaQuery);
typedQuery.setHint("javax.persistence.loadgraph", entityGraph);
Post post = typedQuery.getSingleResult();

В каждом из них тип графика дается как подсказка. В то время как в первом примере мы использовали Map, в двух последующих примерах мы использовали метод setHint().

7. Заключение

В этой статье мы рассмотрели использование графа сущностей JPA для динамического получения сущности и ее ассоциаций.

Решение принимается во время выполнения, в котором мы выбираем загружать или не загружать связанную ассоциацию.

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

Как обычно, весь код доступен на GitHub.