Entity Graph

Существуют две стратегии загрузки полей сущности: FetchType.EAGER и FetchType.LAZY. С помощью Entity Graph можно менять стратегию во время выполнения программы.

Модель

Продолжим эксперименты с нашей моделью (пост с тегами и картинками), только теперь над сущностью Post зададим @NamedEntityGraph:

@NamedEntityGraphs({
        @NamedEntityGraph(
                name = "post-entity-graph",
                attributeNodes = {
                     @NamedAttributeNode(value = "images")
                }
        )
})
@Entity
public class Post {
    @Id
    @GeneratedValue(strategy = GenerationType.SEQUENCE)
    private long id;

    private String title;

    @OneToMany(mappedBy = "post", cascade = CascadeType.ALL)
    private List<Image> images = new ArrayList<>();

    @ElementCollection
    private Set<String> tags = new HashSet<>();

    public Post(String title) {
        this.title = title;
    }

    public void addImage(Image image) {
        image.setPost(this);
        this.images.add(image);
    }
   // getters/setters/constructor/toString
}

Картинка Image:

@Entity
public class Image {
    @Id
    @GeneratedValue(strategy = GenerationType.SEQUENCE)
    private long id;

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

    //getters/setters/constructors/toString 
}

@NamedEntityGraph

Каждая аннотация @NamedEntityGraph (а их может быть несколько) задает свою стратегию загрузки полей. Те поля, которые перечислены в attributeNodes получают fetch = FetchType.EAGER.

Как работает  загрузка по умолчанию без Entity Graph

Известно, что по умолчанию коллекции загружаются лениво (FetchType.LAZY), то есть при выборке постов:

@Test
@DisplayName("если не использовать EntityGraph, коллекции не загружаются")
public void givenDefaultFetchStrategy_whenFind_thenCollectionsAreLazy() {

    HibernateUtil.doInHibernate(session -> {
        Post post = session.find(Post.class, 1l);
    });
}

В консоли мы увидим select только из таблицы posttags и images не загружаются):

select post0_.id as id1_1_0_, post0_.title as title2_1_0_ 
   from Post post0_ where post0_.id=?

Теперь с Entity Graph

Если же использовать Entity Graph, то загрузятся еще и коллекции, которые упомянуты в графе в качестве атрибута.

Статический Entity Graph

Сначала используем граф  post-entity-graph, который задали в аннотации.

@Test
@DisplayName("если использовать EntityGraph, загружаются images")
public void givenEntityGraph_whenFind_thenImagesAreEager() {
    HibernateUtil.doInHibernate(session -> {
        Map<String, Object> properties = new HashMap<>();
        properties.put("javax.persistence.fetchgraph", session.getEntityGraph("post-entity-graph"));
        Post post = session.find(Post.class, 1l, properties);

    });
}

В нем указано, что нужно загружать images, что и происходит:

select post0_.id as id1_1_0_, post0_.title as title2_1_0_, images1_.post_id as post_id3_0_1_,
      images1_.id as id1_0_1_, images1_.id as id1_0_2_, images1_.post_id as post_id3_0_2_, 
      images1_.url as url2_0_2_ 
from Post post0_ left outer join Image images1_ 
     on post0_.id=images1_.post_id where post0_.id=?

Динамический Entity Graph

Но не обязательно задавать граф в аннотации, можно сформировать его на ходу. Создадим граф, загружающий теги, и выполним find():

@Test
@DisplayName("если создать динамически EntityGraph, загружаются tags")
public void givenDynamicEntityGraph_whenFind_thenTagsAreEager() {

    HibernateUtil.doInHibernate(session -> {

        Map<String, Object> properties = new HashMap<>();
        EntityGraph<Post> postGraph = session.createEntityGraph(Post.class);
        postGraph.addAttributeNodes("tags");
        properties.put("javax.persistence.fetchgraph", postGraph);
        Post post = session.find(Post.class, 1l, properties);

    });
}

Выше мы задали граф с атрибутом tags.

В консоли видно, что делается выборка не только из таблицы постов Post, но и тегов Post_tags:

select post0_.id as id1_1_0_, post0_.title as title2_1_0_, 
       tags1_.Post_id as Post_id1_2_1_, tags1_.tags as tags2_2_1_ 
       from Post post0_ left outer join Post_tags tags1_ 
       on post0_.id=tags1_.Post_id where post0_.id=?

До сих пор мы выполняли запросы методом find(). Теперь сделаем то же самое с помощью createQuery():

Как указать Entity Graph в Query

@Test
@DisplayName("если использовать EntityGraph, загружаются images")
public void givenEntityGraph_whenQuery_thenImagesAreEager() {
    HibernateUtil.doInHibernate(session -> {

        EntityGraph entityGraph = session.getEntityGraph("post-entity-graph");
        Post post = session.createQuery("select p from Post p where p.id = :id", Post.class)
                .setParameter("id", 1l)
                .setHint("javax.persistence.fetchgraph", entityGraph)
                .getSingleResult();
    });
}

Как видите, граф мы указали в методе setHint().

Типы Entity Graph: fetch vs load

Стоит упомянуть еще о типах графа. До сих пор мы использовали Entity Graph типа fetch. Его конфигурация полностью переопределяет fetching strategy по умолчанию. Это значит, что если бы, например, в описании класса теги были аннотированы с помощью FetchType.EAGER:

@ElementCollection(fetch = FetchType.EAGER)
private Set<String> tags = new HashSet<>();

то они все равно бы не загружались, если не упомянуты в графе.

Entity Graph же типа load дополняет стратегию по умолчанию. То есть не упомянутые в графе теги загружались бы все равно из-за FetchType.EAGER в описании класса.

Тип графа мы задавали в строках:

properties.put("javax.persistence.fetchgraph", postGraph);

.setHint("javax.persistence.fetchgraph", entityGraph)

Для графа типа load было бы так:

javax.persistence.loadgraph

Протестируем loadgraph. Для этого пометим коллекцию тегов (fetch = FetchType.EAGER):

@Data
@NoArgsConstructor
@Entity
public class Post {
    @Id
    @GeneratedValue(strategy = GenerationType.SEQUENCE)
    private long id;
    private String title;

    @OneToMany(mappedBy = "post", cascade = CascadeType.ALL)
    private List<Image> images = new ArrayList<>();

    @ElementCollection(fetch = FetchType.EAGER)
    private Set<String> tags = new HashSet<>();

    public Post(String title) {
        this.title = title;
    }

    public void addImage(Image image) {
        image.setPost(this);
        this.images.add(image);
    }

  ...
}

А затем извлечем посты с loadgraph, при этом в граф не добавляем никаких атрибутов:

@Test
@DisplayName("если создать loadgraph и сделать теги (fetch = FetchType.EAGER), то загружаются tags")
public void givenLoadGraph_whenFind_thenTagsAreEager() {

    HibernateUtil.doInHibernate(session -> {

        Map<String, Object> properties = new HashMap<>();
        EntityGraph<Post> postGraph = session.createEntityGraph(Post.class);

        properties.put("javax.persistence.loadgraph", postGraph);
        Post post = session.find(Post.class, 1l, properties);

    });
}

Несмотря на то, что атрибуты не добавлены в граф, они извлекаются:

select post0_.id as id1_1_0_, post0_.title as title2_1_0_, tags1_.Post_id as post_id1_2_1_, tags1_.tags as tags2_2_1_ 
from Post post0_ left outer join Post_tags tags1_ on post0_.id=tags1_.Post_id where post0_.id=?

Если добавить с тест строку, то загрузятся и теги, и images:

postGraph.addAttributeNodes("images");

SQL будет такой:

select post0_.id as id1_1_0_, post0_.title as title2_1_0_, images1_.post_id as post_id3_0_1_, images1_.id as id1_0_1_, images1_.id as id1_0_2_, images1_.post_id as post_id3_0_2_, images1_.url as url2_0_2_, tags2_.Post_id as post_id1_2_3_, tags2_.tags as tags2_2_3_ 
from Post post0_ 
left outer join Image images1_ on post0_.id=images1_.post_id 
left outer join Post_tags tags2_ on post0_.id=tags2_.Post_id where post0_.id=?

Подграфы

Entity Graph может быть сложным. Если загружаемая сущность Image в свою очередь тоже ссылается на какую-нибудь коллекцию colors или другую сущность, то для нее тоже можно задать вложенный Entity Graph:

@NamedEntityGraphs({
        @NamedEntityGraph(
                name = "post-entity-graph",
                attributeNodes = {
                        @NamedAttributeNode(value = "images", subgraph = "colors-subgraph")
                },
                subgraphs = {
                        @NamedSubgraph(
                                name = "colors-subgraph",
                                attributeNodes = {
                                        @NamedAttributeNode("colors")
                                }
                        )
                }
        )
})
В коде коллекция colors отсутствует, выше просто теоретический пример синтаксиса.

Итоги

Код примера есть на GitHub.

Еще об Entity Graph в Spring Data.

 

Entity Graph: 6 комментариев

  1. Относительно раздела 4 статьи.
    Здесь [https://www.baeldung.com/jpa-entity-graph] в разделе 6.1 сказано, что помеченные как EAGER по атрибуты все же загружаются.

    1. да, все верно, у меня также сказано: если пометить в Post коллекцию тегов как (fetch = FetchType.EAGER) и не упоминать теги в loadgraph, то они все равно загрузятся. Потому что loadgraph дополняет, а не перезаписывает FetchType.EAGER-атрибуты. Все EAGER, которые были в посте, остаются в loadgraph.

  2. Вот цитата из статьи где говорится о fetchgraph:
    «…если бы, например, в описании класса теги были аннотированы с помощью FetchType.EAGER … то они все равно бы НЕ загружались, если не упомянуты в графе».
    Но, насколько я понял из 6.1 [https://www.baeldung.com/jpa-entity-graph] (выделенное жирным шрифтом место), в Hibernate, при установленном режиме fetchgraph, атрибуты помеченные EAGER будут загружаться, даже если они не перечислены в EntityGraph-е.

    1. А, про fetchgraph речь.
      Да, действительно, у них сказано, что Hibernate (несмотря на спецификацию JPA) все равно загружает «attributes statically configured as EAGER» (поля с fetch = FetchType.EAGER, если правильно понимаю).
      Но, насколько я тестирую с hibernate 5.5.7.Final, аннотация FetchType.EAGER не дает загрузки, если в fetchgraph поле не упомянуто. Только что проверено и для поля OneToMany в Post, и для ManyToOne в Image. И для ElementCollection.

      1. Вот как. Интересное наблюдение. Наверное тогда loadgraph безопаснее использовать (по крайней мере в том кейсе которым я сейчас занимаюсь, с выключенным open session in view).

Добавить комментарий

Ваш адрес email не будет опубликован. Обязательные поля помечены *