Entity Graph в Spring Data JPA

С помощью Entity Graph можно задать для каждого запроса свою стратегию загрузки данных: LAZY либо EAGER. Рассмотрим, как это сделать на примере простой модели — поста с коллекциями картинок и тегов.

Модель

Класс Post с коллекциями:

@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<>();

   //getters/setters/constructors 

}

Теги — просто строки, а картинки — класс:

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

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

   //getter/setter/constructor
}

Нас будет интересовать загрузка коллекций тегов и картинок при выборе (select) поста. Как известно, по умолчанию они загружаются лениво.

@NamedEntityGraphs

Но у нас это не так, поскольку пост аннотирован @NamedEntityGraphs с одним графом @NamedEntityGraph, в котором сказано, что картинки следует загружать EAGER.

attributeNodes

Потому что всё, что указано в  attributeNodes будет загружаться EAGER.

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

Ниже рассмотрим, как указать в запросе нужный граф.

Entity Graph в Derived Query Methods

Делается это просто. Причем можно как сослаться на граф, прописанный в аннотации выше, так и создать свой на ходу.

Как сослаться в запросе на существующий граф

Для этого метод репозитория аннотируем с помощью @EntityGraph и укажем внутри имя графа (post-entity-graph — так называется наш граф в аннотации @NamedEntityGraph выше):

public interface PostRepository extends JpaRepository<Post, Long>, PostRepositoryCustom {

    @EntityGraph(value = "post-entity-graph")
    List<Post> findByTitle(String title);

   //другие методы


}

Проверим, что при использовании этого метода оператор select выбирает не просто посты, а посты с картинками (так что поле images выбранного Post будет  заполнено):

@Test
@DisplayName("если использовать готовый EntityGraph, images загружаются")
public void givenEntityGraph_whenFind_thenImagesAreEager() {
    List<Post> posts = postRepository.findByTitle( "topic1");
}

В консоли видим выполняемый select:

select post0_.id as id1_1_0_, images1_.id as id1_0_1_, 
    post0_.title as title2_1_0_, images1_.post_id as post_id3_0_1_, 
    images1_.url as url2_0_1_, images1_.post_id as post_id3_0_0__, images1_.id as id1_0_0__ 
from post post0_ left outer join image images1_ 
on post0_.id=images1_.post_id where post0_.title=?

В настройках включено:

spring.jpa.show-sql=true

Но не обязательно аннотировать Post кучей @NamedEntityGraphs, граф можно собрать и на ходу.

Как задать граф с нуля прямо в запросе

Делается это вот таким нехитрым способом:

public interface PostRepository extends JpaRepository<Post, Long>, PostRepositoryCustom {

   // метод
  
    @EntityGraph(attributePaths = {"tags"})
    Optional<Post> findById(Long id);

   //еще метод

}

Теперь мы выбрали теги (tags) в качестве загружаемого EAGER поля, и можно проверить в тесте, что оператор select действительно выбирает теги:

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=?

Entity Graph с @Query

Использовать граф с @Query-запросом тоже не проблема:

public interface PostRepository extends JpaRepository<Post, Long>, PostRepositoryCustom {
 
    //метод
    //метод

    @EntityGraph(value = "post-entity-graph")
    @Query("SELECT p FROM Post p where p.id=:id")
    Post getPostWithImages(Long id);
}

В консоли опять же получим выбор поста с картинками:

select post0_.id as id1_1_0_, images1_.id as id1_0_1_, post0_.title as title2_1_0_, 
      images1_.post_id as post_id3_0_1_, images1_.url as url2_0_1_, 
      images1_.post_id as post_id3_0_0__, images1_.id as id1_0_0__ 
from post post0_ left outer join image images1_ 
on post0_.id=images1_.post_id where post0_.id=?

Entity Graph с кастомными методами

Поскольку Entity Graph применим с чистым JPA и Hibernate, с кастомными методами тоже его можно применить не специфичным для Spring способом.

Создадим пользовательский репозиторий с методом:

public class PostRepositoryImpl implements PostRepositoryCustom {
    @PersistenceContext
    private EntityManager em;

    @Override
    public List<Post> findByTitleDesc(String title) {
       return  em.createQuery("SELECT p FROM Post p WHERE p.title = :title order by p.title desc", Post.class)
                .setParameter("title", title)
                .setHint("javax.persistence.fetchgraph", em.getEntityGraph("post-entity-graph"))
                .getResultList();
    }
}

Как видно в коде выше, граф применяется с помощью метода setHint().

SQL:

select post0_.id as id1_1_0_, images1_.id as id1_0_1_, post0_.title as title2_1_0_, 
images1_.post_id as post_id3_0_1_, images1_.url as url2_0_1_, 
images1_.post_id as post_id3_0_0__, images1_.id as id1_0_0__ 
   from post post0_ left outer join image images1_ 
on post0_.id=images1_.post_id where post0_.title=? 
order by post0_.title desc

Больше о графах в Hibernate есть тут (а также о типах графа fetch и load). В Spring по умолчанию используется тип графа  FETCH.

Как сменить тип графа в Spring с FETCH на LOAD

Если вдруг тип Entity Graph по умолчанию (FETCH) не устраивает, то изменить тип можно так:

@EntityGraph(value = "post-entity-graph", type = EntityGraph.EntityGraphType.LOAD)
List<Post> findByTitle(String title);
В FETCH-графе атрибуты переопределяют все настройки полей, указанные в классе. А в LOAD — дополняют.

Итоги

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

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

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