受where子句

时间:2020-06-17 17:15:00

标签: java spring hibernate jpa spring-data-jpa

使用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> {
}

最后,为了说明问题,我在应用程序启动时执行了一些代码:

  • 创建3个标签Tag ATag BTag 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));
        });
    }
}

期望

我希望在第一个查询中,只会加载文章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 BTag 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执行另一个查询,以加载所有相关标签。

问题

  • 这是带有实体图的JPA的预期默认行为吗?渴望加载实体时要考虑任何where子句吗?
  • 如何仅显示分配给Tag A的文章(文章A和B),但它们的相关标签的 all 个标签却都被急切加载,从而达到预期的效果?

1 个答案:

答案 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!");
      }
    }
相关问题