【问题标题】:Spring Boot - searching category treeSpring Boot - 搜索类别树
【发布时间】:2017-07-23 21:44:26
【问题描述】:

我正在编写基本上是广告服务的 Spring Boot 应用程序。在数据库中(我使用 PostgreSQL 方言)我存储类别树。每个类别都包含对其直接父级的引用。每个广告都有自己的类别,为了解决问题,您可以仅将广告分配给叶类别。

下面的图像捕获了结构。

Category tree

现在我需要从数据库中检索特定类别的广告。但是假设我要检索类别 E 的所有广告。这意味着我要检索类别 E 的子类别的广告,但不一定是直接子类别的广告。在这种情况下,我想检索类别 G、H、I 和 J 的广告,因为它们都是叶子并且都是类别 E 的子节点。

如何通过 Spring 存储库实现它?以下是此示例的简化实体类。

类别...

@Entity
@Table(name = "categories")
public class Category {

    @Id
    @Column(name = "cat_id")
    private int id;

    @Column(name = "cat_name", length = 50, nullable = false, unique = true)
    private String name;

    @ManyToOne(cascade = CascadeType.ALL)
    @JoinColumn(name = "cat_parent", referencedColumnName = "cat_id", nullable = false)
    private Category parent;

    ...
}

...和广告。

@Entity
@Table(name = "advertisements")
public class Advertisement {

    @Id
    @Column(name = "adv_id")
    private long id;

    @Column(name = "adv_title", length = 100, nullable = false)
    private String title;

    @Column(name = "adv_description", length = 5000, nullable = false)
    private String description;

    @ManyToOne(cascade = CascadeType.ALL)
    @JoinColumn(name = "adv_category", referencedColumnName = "cat_id", nullable = false)
    private Category category;

    @ManyToOne(cascade = CascadeType.ALL)
    @JoinColumn(name = "adv_owner", referencedColumnName = "user_id", nullable = false)
    private User owner;

    ...
}

【问题讨论】:

  • 您可以使用 Postgres 公用表表达式 (CTE) 编写本机查询。请参阅official documentation
  • @manish 我没用过,我去看看。我想这与那些递归查询有关吗?看起来不是特别容易。
  • 有两种方法可以创建分层数据结构。当结构中的对象持有对其直接父对象的引用时,该结构称为Adjacency list。另一种选择是将结构建模为Nested set。在嵌套集中查找叶节点很简单,并且需要非常简单的 SQL 查询。然而,嵌套集的构造是复杂的,并且只有在层次结构树大部分是静态的并且不需要经常更改时它们实际上才有用。请仔细阅读这两个模型,看看嵌套集是否可以解决您的问题。
  • @manish 好吧,一旦我创建了类别树,我可能就不会更改它了。这只是我的硕士论文的项目,它不需要完美。所以Nested set听起来是可行的解决方案。尽管如此,Adjacency tree 似乎更加优雅。我找到了一个本地 SQL 查询来从根检索所有叶子,但我没有修改它,所以它会返回任何子树的叶子节点。

标签: java spring postgresql spring-boot repository


【解决方案1】:

这可能不是最优雅的方法,但您可以先收集类别包,然后使用 in 子句 查询广告存储库,将结果限制为仅这些类别中的广告。

首先将父/子关联的另一端添加到 Category 对象(如果它不存在),然后创建一个方法,该方法返回一个包含该类别和所有子(不仅仅是直接子)的集合,例如:

@JoinColumn(name = "cat_parent")
@OneToMany
Set<Category> children;
... 
public Set<Category> collectLeafChildren() { 
   Set<Category> results = new HashSet<>(); 
   if (children.isEmpty()) { 
      results.add(this); 
   } else { 
      children.forEach(child -> { 
        results.addAll(child.collectLeafChildren()); 
      }); 
   } 
   return results; 
}

然后为Advertisement 存储库创建一个新方法,该方法返回给定类别集的所有广告,例如:

List<Advertisement> findByCategoryIn(Set<Category> categories);

并使用那些:

Set<Category> categories = category.collectLeafChildren();
List<Advertisement> advertisementRepository.findByCategoryIn(categories);

【讨论】:

  • 这个好像是最简单的解决方案,一点也不差。该项目不需要最好的。问题是,我已经映射了关联的另一端(父级)。听说两头都搞不好。我可以删除父级,但我不确定将来是否需要它...
  • 很高兴知道,我不确定为什么持有关联的两端会成为问题?我已经完成了几次并且侥幸成功了 ;) 出于性能考虑,默认情况下,父关系 (ManyToOne) 是热切加载的,而子关系 (OneToMany) 是惰性的。您可以使用实体图或在查询中加入获取进一步控制这些关系的映射方式
  • 我不知道,我刚接触 JPA 的东西,但是我的老师曾经告诉我,我们应该避免持有两端的关联,一个就足够了。顺便说一句,我需要稍微改变一下你的方法——我只需要给定子类的叶子类别,我不需要介于两者之间的类别。 public Set&lt;Category&gt; collectLeafChildren() { Set&lt;Category&gt; results = new HashSet&lt;&gt;(); if (children.isEmpty()) { results.add(this); } else { children.forEach(child -&gt; { results.addAll(child.collectLeafChildren()); }); } return results; } 抱歉格式化。
  • 我想维护两端有点繁琐。例如,如果您要向父母添加一个新孩子,您应该确保孩子引用父母并确保孩子在父母的孩子包中。用新的代码示例更新了答案(仅叶子)
  • @Patryk - 您最终是否使用此解决方案或其他解决方案来解决您的问题?如果对你有帮助请采纳答案,谢谢!
猜你喜欢
  • 1970-01-01
  • 2014-11-04
  • 2018-06-08
  • 1970-01-01
  • 2014-10-17
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
相关资源
最近更新 更多