使用Spring Data JPA和Hibernate(带有Postgres数据库)时,我在使用JPA实体图功能来按我期望的方式渴望加载数据时遇到了一些麻烦。为了说明,我提出了一个简单的例子。我有一个包含 Articles 和 Tags 的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> {
}
最后,为了说明问题,我在应用程序启动时执行了一些代码:
Tag A
,Tag B
和Tag C
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));
});
}
}
我希望在第一个查询中,只会加载文章A和B,但是会显示所有的分配标签。在第二个查询中,我希望加载 all 个文章,以及 all 个相关标签。即我希望输出看起来像这样:
--- 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
子句后,实际上只渴望加载集合中作为存储库方法的参数提供的标签。在这种情况下,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条件以筛选出不符合条件的文章; 然后使用已加载商品的ID执行另一个查询,以加载所有相关标签。
where
子句吗?Tag A
的文章(文章A和B),但它们的相关标签的 all 个标签却都被急切加载,从而达到预期的效果? 答案 0 :(得分:1)
看起来这是Spring-Data问题。标签过滤器不应使用与获取实体图相同的联接别名。您可以通过使用执行EXISTS子查询的Spring-Data Specification来实现过滤器,来解决此问题。类似于以下内容
setSelectedItems(api) {
if (this.productDataForAggrid.length == api.getDisplayedRowCount()) {
for (let i = 0; i < this.value.length; i++) {
let node = api.getRowNode(this.value[i].productId.toString());
if (node) {
node.setSelected(true);
}
}
this.gridApi.sizeColumnsToFit();
clearInterval(window.watcher);
window.watcher = 0;
//console.log("ready!");
}
}