商品分类模块设计和实现优化

上一篇:让swagger文档支持后台分组校验

现有表设计

先来看下目前商品模块的表设计,对照要实现的商品模块查询要求,看看这样的设计合不合理?

商品表中的分类id字段作为外键,指向了分类表的主键,但要注意,它只取三级分类,也就是分类表中维护的叶子节点的分类数据。这种设计在电商项目中也很常见。因为用户更倾向于用更具体的分类来检索和定位自己想要的商品嘛。

现在我们再来看商品前台和后台查询的几个场景:

  • 后台管理员可以按任意一级的分类来查询商品
  • 前台用户可以查看每个大类(一级分类)下的最新上架的商品列表(前6个)
  • 前台用户按三级分类精确检索商品列表

主要看前两个查询,目前的设计,我们需要对外层分类尤其是一级分类进行多级遍历获取三级子分类,再去商品表里按三级分类来匹配查询。这里用父分类获取三级分类列表比较麻烦,除了要多层遍历外,还要做子类的去重处理,很麻烦。

调整表设计

现在我们思考怎么调整设计,来让基于分类的查询更容器实现。为了支持前台用户可以用大类检索商品,我们很容易想到在商品表中加一个一级分类的字段root_categoty_id。这确实解决了按一级分类查询的问题,但还是没有解决按任意层级比如二级分类来查询商品,很显然冗余一级、二级分类id到商品表,增加了分类变动的维护开销,因为比起分类数据,商品的数据将会是几个数量级的,在有调整分类的需求时,这无疑会动大量的商品记录进行分类字段的更新。

那有没有什么更好的设计方案呢?

既然我们的分类考虑到复杂度和用户使用的习惯,只设计到三级,那我们可以对三级分类的记录存储时,除了pid可以引用到二级分类,再加一个root_id字段来引用一级分类。这样对于按照一级、二级分类查询三级分类的需求,实现起来就很简单了:传入一级按root_id匹配,传入二级用pid匹配即可。

调整分类模块实现

pdm新加字段

修改pdm模型,为分类表增加一个字段:

idea数据库工具连接本地h2数据库,右键新增column:

执行mybatis生成器

放开build.gradle中下面注解:

刷新下gradle配置,双击重新执行生成任务:

in查询代替递归查询

前面的小节商品分类接口功能完善(拖拽、redis整合),我们在实现分类层级的遍历和更新时,采用的是递归的处理方式,即获取到pid对子分类进行操作,同样再以子分类的id作为下一个层级的pid进行递归操作,这种执行效率的性能问题在于当分类条目变多,发送的sql语句的次数相应也增多,网络请求也变多,能不能每个层级只执行一次查询,而不用对每个条目都进行sql查询,自然我们想到了使用in条件查询,即,把每一层级的所有分类id都查出来,作为下一层级的pid,通过in查询来实现。因为相比于用户数据,后台分类数据量不会很大,且层级最多3级,也就是说真实场景下一个大类中的三级分类顶多几百条,用in查询相比发送上百条sql语句的效率自然是有巨大的提升的。因此,我们对之前的查询进行改造:

java 复制代码
package com.xiaojuan.boot.dao.mapper;

import ...

public interface CustomCategoryMapper {

    ...

    @Update("<script>update TB_CATEGORY set LEVEL = #{level} + 1 where PID in <foreach collection='pids' item='pid' open='(' separator=',' close=')'>#{pid}</foreach></script>")
    int updateChildrenLevel(@Param("pids") List<Long> pids, @Param("level") int level);
    @Update("<script>update TB_CATEGORY set ROOT_ID = #{rootId} where ID in <foreach collection='ids' item='id' open='(' separator=',' close=')'>#{id}</foreach></script>")
    int batchUpdateRootId(@Param("ids") List<Long> ids, @Param("rootId") long rootId);

    @Select("<script>select id from TB_CATEGORY where PID in <foreach collection='pids' item='pid' open='(' separator=',' close=')'>#{pid}</foreach></script>")
    List<Long> listChildIds(@Param("pids") List<Long> pids);
}

这里我们采用的注解的形式足以应对简单的查询了,只不过这里通过<script>标签脚本的形式我们增加了<foreach标签来支持in查询传入列表参数的形式。看下以上三个方法的实现意图:

  • updateChildrenLevel

    支持批量的更新某个分类或者某个层级下所有的子分类的level字段值,则在级联更新移动分类后的子分类层级会用到。

  • listChildIds

    用于批量的查询某个分类或者某个层级下所有的子分类的id列表,在查看某个分类下面的层级深度以及需要按层级维度来进行批量操作时会用到。

  • batchUpdateRootId

    在我们为分类表引入了冗余的一级分类字段后,在级联更新移动分类后的该字段时会用到。

再看通过in语句改造后的CategoryServiceImpl类中的级联操作方法:

java 复制代码
public int cascadeCalcDepth(long pid) {
    int length = 0;
    List<Long> pids = Collections.singletonList(pid);
    do {
        pids = customCategoryMapper.listChildIds(pids);
        length++;
    } while (!CollectionUtils.isEmpty(pids));
    return length - 1;
}

这样我们只需要每层执行一次查询,而先前的递归遍历效率实在是低下。

再看级联更新层级字段的方法改造后:

java 复制代码
private void cascadeUpdateLevel(long pid, int level) {
    List<Long> pids = Collections.singletonList(pid);
    while (level < 3) {
        customCategoryMapper.updateChildrenLevel(pids, level);
        if (level == 2) break;
        pids = customCategoryMapper.listChildIds(pids);
        level++;
    }
}

引入冗余root_id后的实现调整

在进行分类新增时,如果判断新增的是三级分类,就要设置上该字段:

java 复制代码
public void addCategory(CategoryAddDTO categoryAddDTO) {
    // 前置判断逻辑省略
	...
    Category category = new Category();
    ...

    // 如果新增的是三级分类就查找下一级分类
    if (level == 3) {
        long rootId = customCategoryMapper.getPid(pid);
        category.setRootId(rootId);
    }

    ...
    categoryMapper.insertSelective(category);

    ...
}

在移动分类方法实现的最后要级联更新root_id字段,这里多加一个判断逻辑:如果被移动的节点拖拽放置后,层级没有变且根分类没有变,则不进行root_id字段的级联更新:

java 复制代码
@Transactional
@Override
public void moveCategories(CategoryMoveDTO moveCategoryDTO) {
    // 前面逻辑省略
    ...

    // 获取原来的层级
    long level = customCategoryMapper.getLevel(dragId);
    long oldRootId = getRootId(dragId);
    categoryMapper.updateByPrimaryKeySelective(updateDrag);

    // 级联处理被拖拽的子分类的层级
    if (level != updateDrag.getLevel()) {
        cascadeUpdateLevel(dragId, updateDrag.getLevel());
    }

    // 更新拖拽后的节点的三级节点的rootId
    long rootId = getRootId(dragId);
    if (level != updateDrag.getLevel() || oldRootId != rootId) {
        cascadeUpdateRootId(Collections.singletonList(dragId), updateDrag.getLevel(), rootId);
    }

    stringRedisTemplate.delete(REDIS_CATEGORY_DATA_KEY);
}

private long getRootId(long id) {
    long tempId;
    do {
        tempId = id;
        id = customCategoryMapper.getPid(id);
    } while (id != 0);
    return tempId;
}

private void cascadeUpdateRootId(List<Long> ids, int level, long rootId) {
    Long rid;
    for (int i = level; i <= 3; i++) {
        rid = i < 3 ? null : rootId;
        customCategoryMapper.batchUpdateRootId(ids, rid);
        if (i == 3) break;
        ids = customCategoryMapper.listChildIds(ids);
    }
}

在分类移动后,对拖拽的分类节点和子节点更新它们的root_id字段,如果level为3,则更新为具体的值,否则更新为null,因为我们希望一级和二级分类的记录该字段留空。

相关推荐
程序员大金12 分钟前
基于SpringBoot+Vue+MySQL的在线学习交流平台
java·vue.js·spring boot·后端·学习·mysql·intellij-idea
qq_25183645719 分钟前
基于SpringBoot vue 医院病房信息管理系统设计与实现
vue.js·spring boot·后端
武昌库里写JAVA40 分钟前
Vue3常用API总结
数据结构·spring boot·算法·bootstrap·课程设计
qq_2518364571 小时前
基于springboot vue3 在线考试系统设计与实现 源码数据库 文档
数据库·spring boot·后端
2401_858120531 小时前
古典舞在线交流平台:SpringBoot设计与实现详解
java·spring boot·后端
潘多编程3 小时前
Spring Boot微服务架构设计与实战
spring boot·后端·微服务
2402_857589364 小时前
新闻推荐系统:Spring Boot框架详解
java·spring boot·后端
原机小子4 小时前
Spring Boot框架下的新闻推荐技术
服务器·spring boot·php
2401_857622664 小时前
新闻推荐系统:Spring Boot的可扩展性
java·spring boot·后端
2402_857589365 小时前
Spring Boot新闻推荐系统设计与实现
java·spring boot·后端