Существуют две стратегии загрузки полей сущности: 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 только из таблицы post (а tags и 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") } ) } ) })
Итоги
Код примера есть на GitHub.
Еще об Entity Graph в Spring Data.
Относительно раздела 4 статьи.
Здесь [https://www.baeldung.com/jpa-entity-graph] в разделе 6.1 сказано, что помеченные как EAGER по атрибуты все же загружаются.
да, все верно, у меня также сказано: если пометить в Post коллекцию тегов как (fetch = FetchType.EAGER) и не упоминать теги в loadgraph, то они все равно загрузятся. Потому что loadgraph дополняет, а не перезаписывает FetchType.EAGER-атрибуты. Все EAGER, которые были в посте, остаются в loadgraph.
Пример с loadgraph добавлен.
Вот цитата из статьи где говорится о fetchgraph:
«…если бы, например, в описании класса теги были аннотированы с помощью FetchType.EAGER … то они все равно бы НЕ загружались, если не упомянуты в графе».
Но, насколько я понял из 6.1 [https://www.baeldung.com/jpa-entity-graph] (выделенное жирным шрифтом место), в Hibernate, при установленном режиме fetchgraph, атрибуты помеченные EAGER будут загружаться, даже если они не перечислены в EntityGraph-е.
А, про fetchgraph речь.
Да, действительно, у них сказано, что Hibernate (несмотря на спецификацию JPA) все равно загружает «attributes statically configured as EAGER» (поля с fetch = FetchType.EAGER, если правильно понимаю).
Но, насколько я тестирую с hibernate 5.5.7.Final, аннотация FetchType.EAGER не дает загрузки, если в fetchgraph поле не упомянуто. Только что проверено и для поля OneToMany в Post, и для ManyToOne в Image. И для ElementCollection.
Вот как. Интересное наблюдение. Наверное тогда loadgraph безопаснее использовать (по крайней мере в том кейсе которым я сейчас занимаюсь, с выключенным open session in view).