概念——SPU与SKU

SPU

Standard Product Unit 标准化产品单元:是商品信息聚合的最小单位,是一组可复用、易检索标准化信息的集合,该集合描述了一个产品的特性。

  • 通俗点讲,属性值、特性相同的商品就可以称为一个 SPU。
  • 例如:iPhone 12就是一个 SPU,与商家,与颜色、款式、套餐都无关。

    SKU

    Stock Keeping Unit 库存量单位:即库存进出计量的基本单元,可以是以件、盒等为单位。SKU这是对于大型连锁超市 DC (配送中心)物流管理的一个必要的方法。现在已经被引申为产品统一编号的简称,每种产品均对应有唯一的SKU号。

  • SKU 即库存进出计量的单位, 可以是以件、盒、托盘等为单位。

  • SKU 是物理上不可分割的最小存货单元。在使用时要根据不同业态,不同管理模式来处理。
  • 在服装、鞋类商品中使用最多最普遍。
  • 例如:魅族18的颜色(深空灰等),存储容量(64GB 128GB 256GB)。

    举例:

    iPhoneX 是 SPU、MI8 是 SPU
    iPhoneX 64G 黑曜石是 SKU
    MI8 8+64G 黑色是 SKU

    基本属性【规格参数】与销售属性

    每个分类下的商品共享规格参数,与销售属性。只是有些商品不一定要用这个分类下全部的属性;

  • 属性是以三级分类组织起来的

  • 规格参数中有些是可以提供检索的
  • 规格参数也是基本属性,他们具有自己的分组;
  • 属性的分组也是以三级分类组织起来的;
  • 属性名是确定的,但是值是每一个商品不同决定的;

商品的基础属性是SPU的特性,商品的销售属性是SKU的特性。
image.png
image.png

前序准备

sys_menus.sql 我们在gulimall_admin数据库中执行sys_menus.sql文件,该文件会为我们生成很多需要用到的目录菜单。
注意:
image.png这里的名字根据自己数据库修改。
课件也提供了核心功能的前端代码,可以直接复制,但是建议一步一步跟着视频来。
接口文档地址:https://easydoc.net/s/78237135/ZUqEdvA4/hKJTcbfd

属性分组实现效果

选中一个三级分类,然后可以查出这个分类下已经存在的分组,并可以新增/删除属性分组。
image.png
所以这一我们需要三级分类的分类树(不需要拖拽、修改等功能,仅需要展示即可)

一、前端组件抽取&父子组件交互

1.1 抽取组件

在前端项目modules文件夹下新建一个common文件夹,新建category.vue作为三级分类组件,在其他组件中使用这个三级分类组件。

1.2 父子组件交互

现在我们在其他组件中均使用了/common/category.vue,那么这样会存在问题,我点击三级分类项是category.vue中的事件,怎么样才能传到使用category.vue的其他组件中去呢?目的就是,当三级分类被点击时,我在父组件中需要知道哪个被点击了。
这里就需要用到vue里面的高级用法,父子组件传递数据。category是子组件,导入cate的是父组件

1.2.1 子组件给父组件传递数据——事件机制

  1. 子组件给父组件发送一个事件,事件携带数据。

在../common/category中绑定node-click事件,method:{}中添加nodeclick方法通过this.$emit("事件名",需要携带的数据);发送事件和数据

  1. <el-tree :data="menus" :props="defaultProps" node-key="catId" ref="menuTree" @node-click="nodeclick"></el-tree>
  2. methods:{
  3. nodeclick(data, node, component) {
  4. console.log("子组件category被点击",data, node, component);
  5. // 向父组件发送事件
  6. // $emit("事件名",....所有需要携带的数据)
  7. this.$emit("tree-node-click",data, node, component);
  8. },
  9. }
  1. 父组件接收子组件发送的事件 ```vue

methods:{ // 感知子节点(tree)被点击 treenodeclick(data, node, component) { console.log(“attrgroup感知到category的节点被点击”,data, node, component); console.log(“刚才被点击的菜单名:”, data.name); }, }

  1. ![image.png](https://cdn.nlark.com/yuque/0/2022/png/22423156/1646742654685-1278b79e-429c-42d3-b849-4c28c4cf7e5c.png#clientId=u40ac6fb8-4f3a-4&crop=0&crop=0&crop=1&crop=1&from=paste&height=70&id=u5f2f37bc&name=image.png&originHeight=70&originWidth=1019&originalType=binary&ratio=1&rotation=0&showTitle=false&size=12715&status=done&style=none&taskId=uce102178-71ad-4167-a7ca-65478aacb0c&title=&width=1019)
  2. <a name="ridaV"></a>
  3. # 二、获取分类属性分组
  4. 通过`GET /product/attrgroup/list/{catelogId} ` 来获取属性分组。
  5. <a name="mWEw1"></a>
  6. ## 2.1 修改gulimall-product
  7. ```java
  8. /**
  9. * 列表 原方法只有/list
  10. */
  11. @RequestMapping("/list/{catelogId}")
  12. //@RequiresPermissions("gulimallproduct:attrgroup:list")
  13. public R list(@RequestParam Map<String, Object> params,@PathVariable("catelogId") Long catelogId){
  14. // PageUtils page = attrGroupService.queryPage(params);
  15. // 逆向生成的方法不能查询ID,所以我们需要自己写一个
  16. PageUtils page = attrGroupService.queryPage(params, catelogId);
  17. return R.ok().put("page", page);
  18. }

在AttrGroupService接口中生成该方法,然后在AttrGroupServiceImpl中实现该方法。

  1. public interface AttrGroupService extends IService<AttrGroupEntity> {
  2. PageUtils queryPage(Map<String, Object> params);
  3. PageUtils queryPage(Map<String, Object> params, Long catelogId);
  4. }
  1. @Override
  2. public PageUtils queryPage(Map<String, Object> params, Long catelogId) {
  3. // 前端规定如果传过来的catelogId为0,那么表示没有选中三级分类,则查询所有属性分组
  4. if (catelogId == 0) {
  5. IPage<AttrGroupEntity> page = this.page(
  6. new Query<AttrGroupEntity>().getPage(params),
  7. new QueryWrapper<AttrGroupEntity>());
  8. return new PageUtils(page);
  9. }
  10. else {
  11. // 前端提交的时候还会提交一个参数名,这个参数名是一个模糊查询,可以匹配任意分组字段,所以需要判断
  12. String key = (String) params.get("key");
  13. // select * from pms_attr_group where catelog_id = ? and (attr_group_id = key or attr_group_name = key)
  14. QueryWrapper<AttrGroupEntity> wrapper = new QueryWrapper<AttrGroupEntity>().eq("catelog_id", catelogId);
  15. if (!StringUtils.isEmpty(key)) {
  16. wrapper.and((obj)->{
  17. obj.eq("attr_group_id", key).or().like("attr_group_name", key);
  18. });
  19. }
  20. IPage<AttrGroupEntity> page = this.page(new Query<AttrGroupEntity>().getPage(params), wrapper);
  21. return new PageUtils(page);
  22. }
  23. }

2.2 测试

image.png
image.png
image.png

2.3 前端

点击一级和二级分类不会查询属性分组,只有点击三级分类才会查询.
修改attrgroup.vue,在data中添加一个catId记录目录id,然后分别修改treenodeclick和getDataList方法
getDataList方法中修改请求的url地址,需要动态的获取catelogId(其实就是catId,目录ID);
treenodeclick需要加一个判断,只有是三级分类目录的时候才查询属性分组。

  1. data() {
  2. //这里存放数据
  3. return {
  4. catId: 0,
  5. dataForm: {
  6. key: "",
  7. },
  8. dataList: [],
  9. pageIndex: 1,
  10. pageSize: 10,
  11. totalPage: 0,
  12. dataListLoading: false,
  13. dataListSelections: [],
  14. addOrUpdateVisible: false,
  15. };
  16. },
  17. // 感知子节点(tree)被点击
  18. treenodeclick(data, node, component) {
  19. console.log("attrgroup感知到category的节点被点击",data, node, component);
  20. console.log("刚才被点击的菜单名:", data.name);
  21. if (node.level == 3) { //判断当前点击的节点是否为三级分类
  22. this.catId = data.catId;
  23. this.getDataList(); // 重新插曲属性分组列表
  24. }
  25. },
  26. // 获取属性分组数据列表
  27. getDataList () {
  28. this.dataListLoading = true
  29. this.$http({
  30. url: this.$http.adornUrl(`/gulimallproduct/attrgroup/list/${this.catId}`),
  31. method: 'get',
  32. params: this.$http.adornParams({
  33. 'page': this.pageIndex,
  34. 'limit': this.pageSize,
  35. 'key': this.dataForm.key
  36. })
  37. }).then(({data}) => {
  38. if (data && data.code === 0) {
  39. this.dataList = data.page.list
  40. this.totalPage = data.page.totalCount
  41. } else {
  42. this.dataList = []
  43. this.totalPage = 0
  44. }
  45. this.dataListLoading = false
  46. })
  47. },

2.4 测试

测试前在gulimall_prodcut数据库的pms_attr_group表中加几条记录
image.png
image.png
image.pngimage.png

三、分组新增&修改&删除

3.1 Cascader级联选择器

在添加属性分组的时候,所属分类ID应该是一个选择框,选择框中展示已有的分类ID
image.png
使用EL的Cascader级联选择器,修改attrgroup-add-or-update.vue
image.png
会发现三级目录后面还会有空白的目录,这是因为后端返回的树形目录中,三级目录的children是空数组。需要在调整后端代码,使其返回时三级目录不带有children。
可以通过在实体类的属性上加上@JsonInclude(JsonInclude.Include.NON_EMPTY),实现当该字段非空时,响应才包含此字段。

  1. @JsonInclude(JsonInclude.Include.NON_EMPTY) // 这个注解表示当该属性非空时,响应才包含该字段
  2. @TableField(exist = false) // 这个注解表示改属性不是表里面的
  3. private List<CategoryEntity> children;

3.2 修改-级联选择器回显

点击修改属性分组的时候,发现所属分类ID并没有回显。
image.png这是因为在修改的时候,只查到了当前三级分类的catalogId,还需要查出catelogId的完整路径
image.png

3.2.1 查询当前目录的完整路径

所谓完整路径就是要查出当前目录的所有父目录的catId,再次强调这个目录ID在CategoryEntity中叫catId,在AttrGroupEntity中叫catelogId
首先需要在AttrGroupEntity实体类中添加一个catelogPath字段,用于存储完整路径。

  1. /**
  2. * catelogId的完整路径(包含父菜单)
  3. */
  4. @TableField(exist = false)
  5. private Long[] catelogPath;

AttrGroupController中,修改查询信息的接口,给返回的AttrGroupEntity添加上完整的路径信息

  1. @Autowired
  2. private CategoryService categoryService;
  3. /**
  4. * 信息
  5. */
  6. @RequestMapping("/info/{attrGroupId}")
  7. //@RequiresPermissions("gulimallproduct:attrgroup:info")
  8. public R info(@PathVariable("attrGroupId") Long attrGroupId){
  9. AttrGroupEntity attrGroup = attrGroupService.getById(attrGroupId);
  10. // 希望通过catelogId查到完整路径
  11. Long catelogId = attrGroup.getCatelogId();
  12. Long[] path = categoryService.findCatelogPath(catelogId); // 该方法通过当前分类的catelogId找到完整路径
  13. attrGroup.setCatelogPath(path);
  14. return R.ok().put("attrGroup", attrGroup);
  15. }

因为这里需要通过catelogID(catId)查询其完整路径信息,我们定义一个方法findCatelogPath实现查询完整路径信息。在CategoryService接口中声明该方法,在CategoryServiceImpl中实现该方法

  1. @Override
  2. public Long[] findCatelogPath(Long catelogId) {
  3. List<Long> paths = new ArrayList<>();
  4. findParentPath(paths, catelogId);
  5. return paths.toArray(new Long[0]);
  6. }
  7. private void findParentPath(List<Long> paths, Long catelogId) {
  8. CategoryEntity category = this.getById(catelogId);
  9. if (category.getParentCid() != 0) { // 这里会触发自动拆箱
  10. findParentPath(paths, category.getParentCid());
  11. }
  12. paths.add(catelogId);
  13. }

测试:image.png

3.2.2 前端代码

src\views\modules\product\attrgroup-add-or-update.vue

  1. <template>
  2. <el-dialog
  3. :title="!dataForm.attrGroupId ? '新增' : '修改'"
  4. :close-on-click-modal="false"
  5. :visible.sync="visible"
  6. @closed="dialogClose"
  7. >
  8. <el-form
  9. :model="dataForm"
  10. :rules="dataRule"
  11. ref="dataForm"
  12. @keyup.enter.native="dataFormSubmit()"
  13. label-width="80px"
  14. >
  15. <el-form-item label="组名" prop="attrGroupName">
  16. <el-input
  17. v-model="dataForm.attrGroupName"
  18. placeholder="组名"
  19. ></el-input>
  20. </el-form-item>
  21. <el-form-item label="排序" prop="sort">
  22. <el-input v-model="dataForm.sort" placeholder="排序"></el-input>
  23. </el-form-item>
  24. <el-form-item label="描述" prop="descript">
  25. <el-input v-model="dataForm.descript" placeholder="描述"></el-input>
  26. </el-form-item>
  27. <el-form-item label="组图标" prop="icon">
  28. <el-input v-model="dataForm.icon" placeholder="组图标"></el-input>
  29. </el-form-item>
  30. <el-form-item label="所属分类id" prop="catelogId">
  31. <!-- <el-input v-model="dataForm.catelogId" placeholder="所属分类id"></el-input> -->
  32. <el-cascader
  33. v-model="dataForm.catelogPath"
  34. :options="categorys"
  35. :props="props"
  36. placeholder="试试搜索:手机"
  37. filterable
  38. ></el-cascader>
  39. </el-form-item>
  40. </el-form>
  41. <span slot="footer" class="dialog-footer">
  42. <el-button @click="visible = false">取消</el-button>
  43. <el-button type="primary" @click="dataFormSubmit()">确定</el-button>
  44. </span>
  45. </el-dialog>
  46. </template>
  47. <script>
  48. export default {
  49. data() {
  50. return {
  51. props: {
  52. value:"catId",
  53. label:"name",
  54. children:"children"
  55. },
  56. categorys: [],
  57. visible: false,
  58. dataForm: {
  59. attrGroupId: 0,
  60. attrGroupName: "",
  61. sort: "",
  62. descript: "",
  63. icon: "",
  64. catelogPath: [],
  65. catelogId: 0
  66. },
  67. dataRule: {
  68. attrGroupName: [
  69. { required: true, message: "组名不能为空", trigger: "blur" },
  70. ],
  71. sort: [{ required: true, message: "排序不能为空", trigger: "blur" }],
  72. descript: [
  73. { required: true, message: "描述不能为空", trigger: "blur" },
  74. ],
  75. icon: [{ required: true, message: "组图标不能为空", trigger: "blur" }],
  76. catelogId: [
  77. { required: true, message: "所属分类id不能为空", trigger: "blur" },
  78. ],
  79. },
  80. };
  81. },
  82. methods: {
  83. // 关闭对话框时,清空所选分类
  84. dialogClose() {
  85. this.dataForm.catelogPath = []
  86. },
  87. getCategorys() {
  88. this.$http({
  89. // url表示我们的请求地址
  90. url: this.$http.adornUrl("/gulimallproduct/category/list/tree"),
  91. method: "get",
  92. }).then(({ data }) => {
  93. // console.log("成功获取到菜单数据。。。", data.data);
  94. this.categorys = data.data;
  95. });
  96. },
  97. init(id) {
  98. this.dataForm.attrGroupId = id || 0;
  99. this.visible = true;
  100. this.$nextTick(() => {
  101. this.$refs["dataForm"].resetFields();
  102. if (this.dataForm.attrGroupId) {
  103. this.$http({
  104. url: this.$http.adornUrl(
  105. `/gulimallproduct/attrgroup/info/${this.dataForm.attrGroupId}`
  106. ),
  107. method: "get",
  108. params: this.$http.adornParams(),
  109. }).then(({ data }) => {
  110. if (data && data.code === 0) {
  111. this.dataForm.attrGroupName = data.attrGroup.attrGroupName;
  112. this.dataForm.sort = data.attrGroup.sort;
  113. this.dataForm.descript = data.attrGroup.descript;
  114. this.dataForm.icon = data.attrGroup.icon;
  115. this.dataForm.catelogId = data.attrGroup.catelogId;
  116. // 查出catelogId的完整路径
  117. this.dataForm.catelogPath = data.attrGroup.catelogPath;
  118. }
  119. });
  120. }
  121. });
  122. },
  123. // 表单提交
  124. dataFormSubmit() {
  125. this.$refs["dataForm"].validate((valid) => {
  126. if (valid) {
  127. this.$http({
  128. url: this.$http.adornUrl(
  129. `/gulimallproduct/attrgroup/${
  130. !this.dataForm.attrGroupId ? "save" : "update"
  131. }`
  132. ),
  133. method: "post",
  134. data: this.$http.adornData({
  135. attrGroupId: this.dataForm.attrGroupId || undefined,
  136. attrGroupName: this.dataForm.attrGroupName,
  137. sort: this.dataForm.sort,
  138. descript: this.dataForm.descript,
  139. icon: this.dataForm.icon,
  140. catelogId: this.dataForm.catelogPath[this.dataForm.catelogPath.length - 1],
  141. }),
  142. }).then(({ data }) => {
  143. if (data && data.code === 0) {
  144. this.$message({
  145. message: "操作成功",
  146. type: "success",
  147. duration: 1500,
  148. onClose: () => {
  149. this.visible = false;
  150. this.$emit("refreshDataList");
  151. },
  152. });
  153. } else {
  154. this.$message.error(data.msg);
  155. }
  156. });
  157. }
  158. });
  159. },
  160. },
  161. //生命周期 - 创建完成(可以访问当前 this 实例)
  162. created() {
  163. this.getCategorys();
  164. },
  165. };
  166. </script>

四、 品牌分类关联与级联更新

首先我们重新执行pms_catelog.sql文件pms_catelog.sql
可以发现前端的分页数据是错误的,这是因为mybatis-plus的分页插件没有配置mybatis-plus分页

4.1 配置mybatis-plus分页

gulimall-product
注意:这里我的是mybatis-plus3.5.1版本,视频里面用的老版本,配置方式不同,根据自己的版本配置。

  1. package com.atguigu.gulimall.gulimallproduct.config;
  2. import com.baomidou.mybatisplus.annotation.DbType;
  3. import com.baomidou.mybatisplus.autoconfigure.ConfigurationCustomizer;
  4. import com.baomidou.mybatisplus.extension.plugins.MybatisPlusInterceptor;
  5. import com.baomidou.mybatisplus.extension.plugins.inner.PaginationInnerInterceptor;
  6. import com.baomidou.mybatisplus.extension.plugins.pagination.optimize.JsqlParserCountOptimize;
  7. import org.mybatis.spring.annotation.MapperScan;
  8. import org.springframework.context.annotation.Bean;
  9. import org.springframework.context.annotation.Configuration;
  10. import org.springframework.transaction.annotation.EnableTransactionManagement;
  11. /**
  12. * @author mrlinxi
  13. * @create 2022-03-09 16:20
  14. */
  15. @Configuration
  16. @EnableTransactionManagement // 开启事务
  17. @MapperScan("com.atguigu.gulimall.gulimallproduct.dao")
  18. public class MyBatisConfig {
  19. /**
  20. * 老版本的分页插件设置,我这里用的新版本的mybatis-plus3.5.1,
  21. */
  22. /*@Bean
  23. public PaginationInterceptor paginationInterceptor() {
  24. PaginationInterceptor paginationInterceptor = new PaginationInterceptor();
  25. // 设置请求的页面大于最大页后操作, true调回到首页,false 继续请求 默认false
  26. paginationInterceptor.setOverflow(true);
  27. // 设置最大单页限制数量,默认 500 条,-1 不受限制
  28. paginationInterceptor.setLimit(1000);
  29. // 开启 count 的 join 优化,只针对部分 left join
  30. paginationInterceptor.setCountSqlParser(new JsqlParserCountOptimize(true));
  31. return paginationInterceptor;
  32. }*/
  33. /**
  34. * 新的分页插件,一缓和二缓遵循mybatis的规则,需要设置 MybatisConfiguration#useDeprecatedExecutor = false 避免缓存出现问题(该属性会在旧插件移除后一同移除)
  35. */
  36. @Bean
  37. public MybatisPlusInterceptor mybatisPlusInterceptor() {
  38. MybatisPlusInterceptor interceptor = new MybatisPlusInterceptor();
  39. PaginationInnerInterceptor paginationInnerInterceptor = new PaginationInnerInterceptor(DbType.MYSQL);
  40. // 设置请求的页面大于最大页后操作, true调回到首页,false 继续请求 默认false
  41. paginationInnerInterceptor.setOverflow(true);
  42. // 设置最大单页限制数量,默认 500 条,-1 不受限制
  43. paginationInnerInterceptor.setMaxLimit(1000L);
  44. // optimizeJoin字段设置(默认为true)是否生成 countSql 优化掉 join 现在只支持 left join
  45. paginationInnerInterceptor.setOptimizeJoin(true);
  46. interceptor.addInnerInterceptor(paginationInnerInterceptor);
  47. return interceptor;
  48. }
  49. // @Bean
  50. // public ConfigurationCustomizer configurationCustomizer() {
  51. // return configuration -> configuration.setUseDeprecatedExecutor(false);
  52. // }
  53. }

4.2 配置品牌模糊查询功能

当我们使用品牌的模糊查询时,前端发送的请求会携带一个名为key的字段。发送的是/list请求,因为模糊查询会存在多条记录匹配。
image.png,默认生成的查询方法没有对这个字段进行处理,所以需要修改BrandServiceImpl实现类的querPage方法。

  1. @Service("brandService")
  2. public class BrandServiceImpl extends ServiceImpl<BrandDao, BrandEntity> implements BrandService {
  3. @Override
  4. public PageUtils queryPage(Map<String, Object> params) {
  5. QueryWrapper<BrandEntity> wrapper = new QueryWrapper<>();
  6. //1 获取key 当我们使用模糊查询时,前端发送的请求会带一个key的字段
  7. String key = (String) params.get("key");
  8. if (!StringUtils.isEmpty(key)) { // 当key不是空串,则代表需要进行模糊匹配
  9. // select * from pms_brand where brand_id = key or name = key
  10. wrapper.eq("brand_id", key).or().like("name", key);
  11. }
  12. IPage<BrandEntity> page = this.page(
  13. new Query<BrandEntity>().getPage(params),
  14. wrapper
  15. );
  16. return new PageUtils(page);
  17. }
  18. }

4.3 品牌关联分类

每一个品牌都会有其对应的分类,但是品牌与分类不是一对一的关系,而是多对多的关系。(比如小米又有手机又有电视机等,手机也有很多品牌)所以在数据库中会有一张中间表(pms_category_brand_relation)记录这种多对多的关系。
image.png
image.png
这里存在两个接口需要编写:获取品牌关联的分类、新增品牌与分类关联关系 开发文档
CategoryBrandRelationController中添加下列方法:

  1. /**
  2. * 获取当前品牌关联的所有分类list
  3. */
  4. //@RequestMapping(value = "/catelog/list", method = RequestMethod.GET)
  5. @GetMapping(value = "/catelog/list")
  6. //@RequiresPermissions("gulimallproduct:categorybrandrelation:list")
  7. public R catelogList(@RequestParam("brandId") Long brandId){
  8. //查询的就是 brandId对应的品牌列表 查询的是pms_category_brand_relation表
  9. List<CategoryBrandRelationEntity> data = categoryBrandRelationService.list(
  10. new QueryWrapper<CategoryBrandRelationEntity>().eq("brand_id", brandId)
  11. );
  12. return R.ok().put("data", data);
  13. }

CategoryBrandRelationService接口中声明方法,CategoryBrandRelationServiceImpl实现

  1. @Service("categoryBrandRelationService")
  2. public class CategoryBrandRelationServiceImpl extends ServiceImpl<CategoryBrandRelationDao, CategoryBrandRelationEntity> implements CategoryBrandRelationService {
  3. @Autowired
  4. BrandDao brandDao;
  5. @Autowired
  6. CategoryDao categoryDao;
  7. @Override
  8. public PageUtils queryPage(Map<String, Object> params) {
  9. IPage<CategoryBrandRelationEntity> page = this.page(
  10. new Query<CategoryBrandRelationEntity>().getPage(params),
  11. new QueryWrapper<CategoryBrandRelationEntity>()
  12. );
  13. return new PageUtils(page);
  14. }
  15. @Override
  16. public void saveDetail(CategoryBrandRelationEntity categoryBrandRelation) {
  17. // 通过前端传过来的brandId和catelogId查出对应的名称
  18. Long brandId = categoryBrandRelation.getBrandId();
  19. Long catelogId = categoryBrandRelation.getCatelogId();
  20. // 根据brandId和catelogId查询品牌名和分类名,存到需要保存的关联对象中
  21. BrandEntity brandEntity = brandDao.selectById(brandId);
  22. CategoryEntity categoryEntity = categoryDao.selectById(catelogId);
  23. categoryBrandRelation.setBrandName(brandEntity.getName());
  24. categoryBrandRelation.setCatelogName(categoryEntity.getName());
  25. // 将管关联对象保存到数据库 这里调用的是CategoryBrandRelationServiceImpl的save方法
  26. this.save(categoryBrandRelation);
  27. }
  28. }

测试:
image.png

4.4 数据一致性

为了方便检索,在点击关联分类的时候会将分类名与品牌名都查出来,这个查询并没有使用到连接查询,而是在pms_category_brand_relation 中进行了冗余存储。那么这样便会存在问题:如果品牌表与分类表中的信息发生了变化,关联表中的信息如何进行数据同步?怎么保证数据的一致性?

4.4.1 品牌名更改—级联修改

修改BrandController的/update方法

  1. @RequestMapping("/update")
  2. //@RequiresPermissions("gulimallproduct:brand:update")
  3. public R update(@Validated(UpdateGroup.class) @RequestBody BrandEntity brand){
  4. // brandService.updateById(brand);
  5. // 为了保证数据库的性能,不使用连接查询,所以在品牌分类关联表中存储了冗余的数据
  6. // 那么在更新品牌信息的时候,一定要考虑数据的一致性
  7. brandService.updateDetail(brand);
  8. return R.ok();
  9. }

同样BrandService与BrandServiceImpl中需要声明/实现该方法

  1. @Autowired
  2. CategoryBrandRelationService categoryBrandRelationService;
  3. /**
  4. * 级联更新,保证其他表中的冗余字段一致
  5. * @param brand
  6. */
  7. @Transactional // 记得在Mybatis配置类上开启事务
  8. @Override
  9. public void updateDetail(BrandEntity brand) {
  10. //
  11. this.updateById(brand);
  12. // 保证冗余字段的数据一致
  13. if (!StringUtils.isEmpty(brand.getName())) {
  14. // 同步更新其他关联表中的数据(冗余存储)
  15. categoryBrandRelationService.updateBrand(brand.getBrandId(), brand.getName());
  16. //TODO 更新其他关联
  17. }
  18. }

这里调用了CategoryBrandRelationService的updateBrand方法,同上操作

  1. @Override
  2. public void updateBrand(Long brandId, String name) {
  3. CategoryBrandRelationEntity relationEntity = new CategoryBrandRelationEntity();
  4. relationEntity.setBrandId(brandId);
  5. relationEntity.setBrandName(name);
  6. // 这里我们自定义了一个更新规则,按照brand_id = brandId更新,relationEntity中带了什么字段就更新哪个字段
  7. this.update(relationEntity, new UpdateWrapper<CategoryBrandRelationEntity>().eq("brand_id", brandId));
  8. }

测试:
image.pngimage.png

4.4.2 分类名修改—级联修改

  1. @RequestMapping("/update")
  2. //@RequiresPermissions("gulimallproduct:category:update")
  3. public R update(@RequestBody CategoryEntity category){
  4. // categoryService.updateById(category);
  5. // 目录品牌级联更新
  6. categoryService.updateCascade(category);
  7. return R.ok();
  8. }

CategoryService声明updateCascade方法,实现类实现。
同样需要用到CategoryBrandRelationService来修改其冗余数据,以保证数据一致性。

  1. @Autowired
  2. CategoryBrandRelationService categoryBrandRelationService;
  3. /**
  4. * 级联更新所有关联的数据
  5. * @param category
  6. */
  7. @Transactional // 记得在Mybatis配置类上开启事务
  8. @Override
  9. public void updateCascade(CategoryEntity category) {
  10. this.updateById(category);
  11. if (!StringUtils.isEmpty(category.getName())) {
  12. categoryBrandRelationService.updateCategory(category.getCatId(), category.getName());
  13. }
  14. }

CategoryBrandRelationService声明updateCategory方法,实现类实现

  1. @Override
  2. public void updateCategory(Long catId, String name) {
  3. this.baseMapper.updateCategory(catId, name);
  4. }

这里跟上面不同,选择另一种方式更新目录信息使用baseMapper(自定义sql语句,上一节是使用wrapper)。在CategoryBrandRelationDao接口中声明该方法(记得给参数使用@Param注解起名字,方便写SQL)

  1. @Mapper
  2. public interface CategoryBrandRelationDao extends BaseMapper<CategoryBrandRelationEntity> {
  3. void updateCategory(@Param("catId") Long catId,@Param("catName") String name);
  4. }

因为有MybatisX的插件,快捷键ALT+SHIFT+ENTER可在CategoryBrandRelationDao中生成关联的<update>mapper标签(SQL还是要自己写)

  1. <update id="updateCategory">
  2. UPDATE `pms_category_brand_relation` SET `catelog_name` = #{catName} WHERE `catelog_id` = #{catId}
  3. </update>