У меня возникли проблемы с использованием функции JPA Entity Graph для загрузки данных ожидаемым образом, используя Spring Data JPA и Hibernate (с базой данных Postgres). Для иллюстрации я привел простой пример. У меня есть приложение Spring Boot, содержащее Статьи и Теги с отношением «многие ко многим» между ними:
@Entity
@NoArgsConstructor
@AllArgsConstructor
@Data
public class Article {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Integer id;
private String title;
@ManyToMany
private Set<Tag> tags;
}
@Entity
@AllArgsConstructor
@NoArgsConstructor
@Data
public class Tag {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Integer id;
private String name;
}
У меня также есть простой TagRepository
, а также ArticleRepository
для помощи при загрузке статей, используя аннотацию @EntityGraph
, чтобы указать, какие свойства я хочу загружать, когда я использую каждый метод репозитория, в данном случае свойство tags
:
@Repository
public interface ArticleRepository extends JpaRepository<Article, Integer> {
@EntityGraph(attributePaths = {"tags"})
Set<Article> findAllByTagsIn(Collection<Tag> tags);
@Override
@EntityGraph(attributePaths = {"tags"})
List<Article> findAll();
}
@Repository
public interface TagRepository extends JpaRepository<Tag, Integer> {
}
Наконец, чтобы проиллюстрировать проблему, я выполняю код при запуске приложения, который:
- Создает 3 тегов ,
Tag A
, Tag B
и Tag C
- Создает 3 статей :
Article A
, которым присваиваются теги A и B Article B
, которому присвоены теги A , B и C Article C
, которому назначен только тег C
- Выполняет от
ArticleRepository.findAllByTagsIn
до получает все статьи, которые назначены Tag A
, нетерпеливо загрузка тегов, а затем печать каждого название статьи и имена присвоенных ей тегов. - Повторяет предыдущий шаг, используя вместо него
ArticleRepository.findAll
.
@Component
public class StartupListener implements ApplicationListener<ContextRefreshedEvent> {
private final ArticleRepository articleRepository;
private final TagRepository tagRepository;
@Autowired
public StartupListener(ArticleRepository articleRepository, TagRepository tagRepository) {
this.articleRepository = articleRepository;
this.tagRepository = tagRepository;
}
@Override
public void onApplicationEvent(ContextRefreshedEvent event) {
Tag tagA = tagRepository.save(new Tag(null, "Tag A"));
Tag tagB = tagRepository.save(new Tag(null, "Tag B"));
Tag tagC = tagRepository.save(new Tag(null, "Tag C"));
articleRepository.save(new Article(null, "Article A", Set.of(tagA, tagB)));
articleRepository.save(new Article(null, "Article B", Set.of(tagA, tagB, tagC)));
articleRepository.save(new Article(null, "Article C", Set.of(tagC)));
printArticles("--- Find all by tags in ---", articleRepository.findAllByTagsIn(Set.of(tagA)));
printArticles("--- Find all ---", articleRepository.findAll());
}
private void printArticles(String title, Collection<Article> articles) {
System.out.println(title);
articles.forEach(article -> {
String tags = article.getTags().stream().map(Tag::getName).collect(Collectors.joining(", "));
System.out.println(String.format("%s: [%s]", article.getTitle(), tags));
});
}
}
Expectation
Я ожидал, что в При первом запросе будут загружены только статьи A и B, но будут отображаться все присвоенных им тегов. Во втором запросе я ожидал, что будут загружены все статей с всеми связанных с ними тегов. Т.е. я ожидал, что результат будет выглядеть так:
--- Find all by tags in ---
Article A: [Tag A, Tag B]
Article B: [Tag A, Tag B, Tag C]
--- Find all ---
Article A: [Tag A, Tag B]
Article B: [Tag A, Tag B, Tag C]
Article C: [Tag C]
Результат
К моему удивлению, с дополнительным предложением where
, добавленным к первому запросу, только теги, которые были в set, предоставленный в качестве аргумента для метода репозитория, действительно был загружен. В этом случае Tag B
и Tag C
были полностью опущены, потому что набор, содержащий только Tag A
, был передан методу репозитория и последующему запросу, который был выполнен. Второй запрос сработал, как я и ожидал, потому что в результирующий запрос не было добавлено where
предложений, окружающих категории:
--- Find all by tags in ---
Article A: [Tag A]
Article B: [Tag A]
--- Find all ---
Article A: [Tag A, Tag B]
Article B: [Tag A, Tag B, Tag C]
Article C: [Tag C]
В конечном итоге выполняются следующие запросы:
select
article0_.id as id1_0_0_,
tag2_.id as id1_2_1_,
article0_.title as title2_0_0_,
tag2_.name as name2_2_1_,
tags1_.article_id as article_1_1_0__,
tags1_.tags_id as tags_id2_1_0__
from
article article0_
left outer join
article_tags tags1_
on article0_.id=tags1_.article_id
left outer join
tag tag2_
on tags1_.tags_id=tag2_.id
where
tag2_.id in (?)
select
article0_.id as id1_0_0_,
tag2_.id as id1_2_1_,
article0_.title as title2_0_0_,
tag2_.name as name2_2_1_,
tags1_.article_id as article_1_1_0__,
tags1_.tags_id as tags_id2_1_0__
from
article article0_
left outer join
article_tags tags1_
on article0_.id=tags1_.article_id
left outer join
tag tag2_
on tags1_.tags_id=tag2_.id
Я немного удивлен, что Hibernate пытается сделать все это с помощью одного запроса. Я ожидал, что он сначала выполнит начальный запрос, чтобы получить все совпадающие статьи, присоединиться к тегам и применить условие where для фильтрации тех, которые не соответствуют критериям; затем выполнить другой запрос, используя идентификаторы загруженных статей для загрузки всех связанных тегов.
Вопросы
- Это Ожидаемое поведение JPA по умолчанию с Entity Graphs? Чтобы учесть любые предложения
where
при активной загрузке сущностей? - Как я могу достичь ожидаемого результата, когда отображаются только статьи, которым присвоено
Tag A
(статьи A и B), но все связанных с ними тегов быстро загружаются?