Spring Boot+Thymeleaf遍历自引用Category实体生成层级下拉选问题
你遇到的分类重复展示问题,本质是findAll()会把所有层级的分类(父分类、子分类、孙子分类等)全部查询出来,你在页面遍历所有分类时,子分类本身会作为顶层遍历项单独输出,同时又会作为父分类的子项输出一次,自然就出现了重复。
1. 调整分类查询逻辑
不需要复杂的自定义JPQL,只需要在CategoryRepository中新增一个查询顶层分类的方法即可:
public interface CategoryRepository extends JpaRepository<Category, Long> { // 查询所有没有父分类的顶层分类 List<Category> findByParentCategoryIsNull(); }
Controller层把原来的findAll()替换为这个方法:
model.addAttribute("categories", categoryRepository.findByParentCategoryIsNull());
2. 调整Thymeleaf页面实现
你原来的写法只支持两级分类,且用div包裹option不符合HTML规范,这里用Thymeleaf递归片段实现无限级层级展示:
<!-- 首先定义递归渲染分类选项的片段 --> <th:block th:fragment="renderCategory(category, level)"> <option th:value="${category.id}" th:utext="${#strings.repeat(' ', level) + category.name}"> </option> <!-- 递归渲染子分类,层级+1 --> <th:block th:each="subCategory : ${category.subCategories}" th:replace="~{::renderCategory(${subCategory}, ${level + 1})}"> </th:block> </th:block> <!-- 下拉框部分 --> <select class="form-control" id="category" name="category"> <option value="">All</option> <!-- 遍历所有顶层分类,初始层级为0 --> <th:block th:each="topCategory : ${categories}" th:replace="~{::renderCategory(${topCategory}, 0)}"> </th:block> </select>
其中#strings.repeat(' ', level)会根据分类层级生成对应的缩进空格,视觉上就可以体现出Drink > Alcohol > Beer的层级关系,同时所有分类都可以被正常选中。
1. 是否需要自定义JPARepository查询,而非直接使用findAll()?
需要。findAll()会拉取全部分类到顶层遍历列表,不仅会导致重复展示,也没有必要。我们只需要获取顶层分类作为根节点,子分类可以通过实体类关联的subCategories属性递归获取,新增的findByParentCategoryIsNull()是Spring Data JPA根据方法名自动生成的查询,不需要手动写JPQL,成本极低。
2. 如何查询分类下包含所有子分类的Item?
提供两种常用实现方案:
方案一:Java代码递归实现(适合层级较浅的场景)
先在Category实体类中新增递归获取所有后代分类ID的方法:
public List<Long> getAllDescendantIds() { List<Long> idList = new ArrayList<>(); idList.add(this.getId()); for (Category sub : subCategories) { idList.addAll(sub.getAllDescendantIds()); } return idList; }
在ItemRepository新增按分类ID集合查询的方法:
public interface ItemRepository extends JpaRepository<Item, Long> { List<Item> findByCategoryIdIn(Collection<Long> categoryIds); }
业务层调用示例:
Category targetCategory = categoryRepository.findById(categoryId).orElseThrow(); List<Long> allCategoryIds = targetCategory.getAllDescendantIds(); List<Item> items = itemRepository.findByCategoryIdIn(allCategoryIds);
方案二:数据库递归查询(适合层级较深的场景)
直接用数据库的递归语法查询,性能更高,不需要把全部分类加载到内存,以MySQL为例,在ItemRepository新增原生查询方法:
@Query(value = "WITH RECURSIVE category_tree AS (" + "SELECT id FROM category WHERE id = :categoryId " + "UNION ALL " + "SELECT c.id FROM category c JOIN category_tree ct ON c.parent_category = ct.id" + ") SELECT i.* FROM item i WHERE i.category IN (SELECT id FROM category_tree)", nativeQuery = true) List<Item> findAllByCategoryIncludeSub(Long categoryId);
内容的提问来源于stack exchange,提问作者mortar-1

