苍穹外卖日记 | Day3 公共字段填充、菜品模块

一、回顾与概述

Day2我们已经完成了员工模块与分类模块,相信大家已经对于最基本的CRUD业务有了一定的掌握了,那么今天我们将会在菜品模块上提升难度,利用AOP与反射设置公共字段填充增强、加入OSS上传文件、联表查询逻辑、参数为集合时如何处理以及批量删除时的动态SQL语句等等。

二、公共字段填充

1.需求分析

在我们各个模块的新增操作和修改操作的Service层中,由于DTO模型中无法传输创建时间、更新时间、创建人、更新人这些字段,难免就会涉及到补充属性的相关操作,但几乎很多表中都有这四个公共属性,补充属性的代码又相对固定,所以我们需要把这些公共字段的填充抽象出来,那么就要用到AOP技术面向切面编程,选择性针对那些需要增强功能的方法。

2.切点和切点表达式

(1)确定切点

我们知道切点就是需要实现该增强功能的方法,我们的增强功能本质上是补充属性,需要调用相关的set方法,我们是Controller层、Service层、Mapper层,那么需要在哪里进行增强呢。那这就取决于我们在哪里可以取到这个set方法 。我们用来取set方法的方式是通过反射 加上切面类的joinPoint,反射可以通过getDeclaredMethod方法得到这个对象的方法,而joinPoint则负责拿到这个对象,那我们的目标就是获取到具备setUpdateTime这些方法的类的对象,而这些类基本上都是entity实体类,即它们的字段往往就跟表里面一一对应,而我们只能在Mapper层设置切点,然后通过joinPoint拿到entity参数对象,再用反射机制获取到方法进行动态代理。

以新增员工方法举例:

p1.Controller层的方法参数是EmployeeDTO,里面压根没有这些公共字段

p2.Service层同上

p3.Mapper层的方法参数就是Employee这种entity实体类了,里面有公共字段的set方法,确定为切点位置

(2)确定切点表达式

在确定切点时我们已经明确了是在各个模块的Mapper层的新增和修改方法上切入。我们知道切点表达式有两种常见方法,一种使用execution进行匹配,但是不同模块的新增和修改方法往往所在接口、方法名和参数各不相同,所以如果用execution需要写多个匹配语句用 || 逻辑连接。另一种常用方法就是使用特定注解,这一种方法更加灵活,可以在需要增强的切点上自习添加注解。

3.编写特定注解

在自定义特定注解上需要加上两个元注解**@Target和@Retention** ,@Target是用来表明注解的适用位置、@Retention用来表明注解的生命周期 ,因为注解我们要放在Mapper层的新增修改方法上,所以就是ElementType.METHOD ,生命周期就设置为常见的运行时RetentionPolicy.RUNTIME 即可。然后还有一点需要注意那就是我们在修改和新增两种方法公共字段填充的逻辑是不一样的,前者不需要补充创建人和创建时间两个字段,后者四个四段都要填充,所以我们需要在增强前进行区别,那就可以用注解的属性进行区分。这里的value属性作用就是用来区分修改还是新增操作 ,OperationType就有UPDATE和INSERT两种取值。这样我们就可以通过切面类的参数joinPoint取到注解的value值,进而判断采取哪种增强逻辑。

4.编写切面类

1.注意添加注解@Component交给IOC容器管理,@Aspect表明切面类

2.必须是@Before ,因为Mapper层的修改、新增方法执行时已经需要用到公共属性字段了,所以必须在方法执行前就进行增强

3.获取目标方法上的注解,拿到属性值,判断是执行修改操作的增强逻辑还是新增操作的。具体要使用增强方法的参数JoinPoint对象,调用getSignature然后必须强转成MethodSignature(不强转就会得到Signature,它是调不了getMethod方法获得Method对象的),调用getMethod方法得到Method对象后再调用getAnnotation拿到注解。

4.拿到目标方法的参数对象,还是需要用到JoinPoint对象,调用getArgs方法,前提可以判断Args不为空增强代码健壮性。一般参数都只有一个所以调用args[0]即可。

5.判断注解中的属性值执行不同的增强逻辑。接下来就是反射逻辑,首先需要使用刚刚拿到的参数对象调用getClass拿到该对象的类,然后调用getDeclaredMethod方法,里面需要传方法名(自定义常量)和返回值.class,最后就是把拿到的方法调用invoke动态代理,目标对象就填之前拿到的参数对象,值照常写就行。

6.在需要增强的方法上加上@AutoFill注解,标注上对应value属性即可。

三、菜品模块

1.产品模型与接口概览

根据分类id查询菜品这个接口在昨天已经完成了,从相关接口概览字面上看只有一个批量删除好像是不同的,其他都跟昨天的模块没什么区别。但是仔细看产品模型还是能发现挺多细节有所不同。分页查询这里的需求可以通过菜品名称、菜品分类、菜品状态模糊查询。新增操作里面还有上传图片需求就需要使用阿里云提供的OSS上传云服务。批量删除的时候传入的是id的集合,参数的注解也有不同。

2.新增菜品

(1)接口文档

新增菜品的请求参数是json,需要封装到DishDTO中,然后还要image,这个就涉及到文件上传了。文件上传就需要另外编写一个Controller类,实现upload方法,请求参数就是file,返回文件上传路径。

(2)文件上传功能实现

1.登录阿里云创建Bucket,记得取消阻止公共访问,然后把读写权限改为公共读写 。同时通过AccessKey获取到AccessKeyId和AccessKeySecret 。在IDEA导入阿里云OSS依赖

2.在application-dev.yml文件 里面配置好自己私密的endpoint,access-key-id,access-key-secret,bucket-name 四个值。然后在application.yml文件里面利用${}传输私密配置的值。

3.编写AliOssUtil工具类实现upload方法

4.声明阿里Oss配置类,用来生成AliOssUtil对象,配合@Bean将它注册到IOC容器里面 。关于SpringBoot引入第三方jar的Bean的三种方式可以移步博客:Click Here

5.根据接口文档编写专门的CommonController实现文件上传功能 (不存在Service层和Mapper层)。刚刚配合@Bean已经注册到的aliOssUtil对象在这里通过自动注入获取到 。请求参数是MultiPartFile ,调用它的getOriginalFilename 可以获取原始文件名,我们需要通过substring方法取到它的后缀类似于.jpg,以此确定文件类型 ,但不能直接用原始文件名直接作为上传文件名,原因是如果原始文件名重复了,那么后上传的就会把先上传的文件覆盖 。因此前缀文件名objectName必须唯一 。此时就可以使用UUID.randomUUID 方法来拼接处唯一的上传文件名。随后调用aliOssUtil的upload方法即可,第一个参数是源文件的字节数据file.getBytes()即可,第二个参数就是上传文件名设置成什么。返回值url直接作为data返回即可。

(3)Controller层

(4)Service层

由于DishDTO传输的数据不全所以需要新建一个DIsh然后利用BeanUtils浅拷贝到DIsh上,补充属性的操作我们在此之前已经放到AOP里面了 。这里就可以先调用dishMapper把dish的数据插入到dish表了,但是新增菜品操作还涉及到了口味,前端传来的DTO里面包含了Flavors集合,需要把这个集合里面的所有flavor批量插入到dish_flavor表 里面,但需要注意的是因为是新增操作,我们压根不知道菜品的id是什么,前端传来的DishFlavor中也没有dish_id这个字段,因此直接把数据插入到dish_flavor会因为找不到这个口味对应的菜品而无法插入报错 。所以我们需要事先取到这个id封装到新建的这个dish里面然后利用lambda表达式让每个口味都setDishId补充dish_id字段后再插入。另外凡是涉及到了多表的增删改操作都要开启事务,由于该Service层的方法涉及到两个表的插入操作所以要加@Transactional注解开启事务,关于如何取到这个id放在Mapper层讲解。

(5)Mapper层

大家可以发现除了@Insert和我们编写的公共字段填充的特定注解@AutoFill,还多了一个注解**@Options** 。在上一层我们提到了在新增操作的时候由于我们压根不知道菜品id是多少,在我们写SQL语句的时候id字段也是用null借助数据库内部的主键自增帮我们填入。但是我们在Service业务逻辑层当中进行新增对应菜品的各种味道的时候,插入语句又需要用到菜品的id。所以这就是@Options的作用,通过参数useGeneratedKeys = true, keyProperty = "id"来取到插入的这一条数据的(主键)菜品id,然后封装回Service层new出来的dish对象。这样dish里面的id字段就有值了。当然如果你要把SQL语句写到xml文件里面也可以进行这样的配置,需要注意此时@Options注解和@Insert就都不用写了,直接在xml文件里面的<insert>内部配置上Options的这两个属性即可。

dishFlavorMapper里面批量插入语句因为不确定dishFlavorList集合里面有多少条flavor,所以要插入的数据是不确定的,因此需要编写动态SQL语句 。这里就是用到了<foreach>这个标签。collection参数里面写的是集合的变量名,item可以随便写主要代指集合中每个对象,后面#{}语句中就需要item.类的属性名进行值的替换。关于#{}和${}两种占位符的区别可以移步Click Here

3.分页查询

(1)接口文档

结合产品原型分析接口文档可以发现请求参数categoryId、name、status都不是必须的,对应了需求的三个查询条件和模糊查询要求。返回的数据data对应了PageResult,然后records里面则对应了DishVO。

(2)Controller层

前端GET请求参数是普通请求参数,使用自定义DishPageQueryDTO封装,调用Service即可。注意分页查询Service层传回来了一定是PageResult,然后把PageResult封装到Result.success参数中作为data返回。

(3)Service层

分页查询的Service层还是那老三样,PageHelper设置分页参数,调用mapper,返回new出来的PageResult。注意分析records里面的数据,它决定了Page的泛型填什么类,也就是分页查询出来每行显示的数据。如果用Dish实体类封装的话你会发现categoryName没法封装进去,所以我们要专门再自定义一个DishVO类封装查询出来的数据。

(4)Mapper层

在分析接口文档和产品原型的时候我们就已经说了这个分页查询操作涉及到了多个可选的请求参数,支持模糊查询。因此需要使用动态SQL ,同时在数据库中没有一张表可以同时具备我们需要返回的数据,即符合DishVO字段的,因此我们需要进行联表查询,查dish表和category表 ,前者可以直接把全部字段查了省事,自动封装交给Mybatis 即可,category表只需要查一个name,但这里需要注意的一处细节是,Mybatis的自动封装是把数据库字段名与你接收封装的类(DishVO)的字段名"相似"的对应封装进去,这种"相似"标准只支持驼峰命名和_下滑杠区别(eg:update_time与updateTime在Mybatis看来是可以识别进行自动封装的,或者是干脆两处字段名一模一样) ,但是我们这里发现分类名称在category表中的字段名是name,而DishVO封装分类名称的字段名是categoryName,这俩差别可远了所以Mybatis自动封装识别不出来。解决的办法就是把从category表中查出来的分类名称取一个别名'categoryName' ,这样就可以了。联表查询使用内连接 即可,基于dish表的category_id和category表的id相同即可。因为在该项目中每个菜品dish肯定对应某个分类category外连接的使用情形一般来说是这个菜品dish它可能不属于任何分类category,category_id就可能为空null,内连接就不会把这个菜品dish查询出来。这样我们想要优先全部显示dish还是category就可以使用外连接了。 后面就是使用if-test 来动态使用查询条件,细节上注意如果不是唯一字段可以加上t1.表明是哪个表 的,#{}里面填的是封装类的字段名

4.批量删除菜品

(1)接口文档

请求参数为普通参数ids,特别注意的是传入的是1,2,3....这样的多个id 。分析产品原型的删除业务逻辑,在售状态下的菜品不准删除,被套餐关联的菜品不准删除 ,此外还有一个逻辑就是每个菜品都对应了多个口味,如果这个菜品被删除了那么对应的口味数据也需要删除。

(2)Controller层

请求参数传来的多个id可以使用集合或数组封装,这里使用集合是因为它的可调用API多一点,到时候处理更加方便。需要注意的是请求参数为普通参数且为一个集合的时候需要加注解@RequestParam。

(3)Service层

业务逻辑层共四个逻辑:

1.起售状态菜品不可删除,把传来的全部菜品id一一遍历,状态在dish表中可以查到,干脆写个selectById方法查*方便后续复用,查到的数据封装到dish中,然后判断dish的状态是否等于自定义常量"ENABLE",是的话就抛出自定义异常,自定义信息即可。

2.被套餐关联的菜品不可删除,菜品和套餐实际上是多对多的关系,一般这种关系在设计数据库的时候会多一个第三方表进行连接它们之间的关系,在这里也就是setmeal_dish表,我们就可以在这张表中根据dish_id查有多少条数据countByDishId,对应了一个菜品关联了多少个套餐count。count大于0就抛出自定义异常和自定义信息。

3.删除菜品基本信息,经过上面两层逻辑过滤后确定了菜品可以删除,调用dishMapper的批量删除方法即可,传入ids。

4.删除菜品对应口味信息dish,菜品一旦删除,它的口味就没必要留着了,需要在dish_flavor表中根据dish_id删除菜品对应的口味,也是批量删除。

(4)Mapper层

DishMapper的selectById方法

SetMealDishMapper的countByDIshId方法 ,这个方法返回的是指定菜品id在这个第三方表setmeal_dish中出现的总次数,是每个菜品出现次数之和,而不是单单的一个菜品的出现次数。因为我们的业务逻辑是如果这个count大于0(即指定菜品集合中至少存在一个菜品有关联的套餐),就直接抛异常停止删除操作,让用户重新选择删除的菜品。

DishMapper的deleteBatch方法

DishFlavorMapper的deleteBatch方法

5.修改菜品信息

(1)接口文档

根据id查询菜品的请求参数为路径参数,修改菜品请求参数为json。修改菜品操作还是分两步,一步回显操作,一步更新更新操作。分析根据id查询菜品的返回数据,存在flavors,可见不只是需要查菜品的基本信息还需要查对应菜品全部口味的信息。

(2)Controller层

路径参数@PathVariable注解要加上,还要json的@RequestBody注解。分析根据id查询菜品的返回值可以发现需要DishVO进行封装返回值。

(3)Service层

回显操作 业务逻辑就两步,第一步在dish表把菜品基本信息根据id查了,第二步在dish_flavor表把菜品对应的全部口味根据dish_id查了,由于最后封装到DishVO里面,前者可以浅拷贝,后者之间setFlavors即可。

更新操作 业务逻辑也是两步,第一步创建一个dish先把前端传来的DishDTO数据浅拷贝进去,然后根据这个dish做更新操作 。第二步就是把对应菜品(dto里面的id)全部口味信息都删除,然后再把dto传来的flavors重新插入进去 。(原因是用户修改菜品的时候它可以新增口味也可以减少口味,甚至每个口味的标签也可以删除,十分复杂,所有干脆把旧信息都删了,把最终确定的新的信息做插入操作即可)。在此之前还要判断口味信息是否为空,因为如果为空的话后续进行批量插入操作的时候因为数据为空会报SQL异常 。需要注意插入口味前需要把每个口味都关联菜品ID ,这一步跟新增菜品的时候有点像,但存在一个关键的区别,那就是新增操作我们是不知道菜品ID 的,因此我们当时在Mapper层insert菜品操作的时候加了Options注解 ,获取到数据库里面新增后的这条数据的菜品ID,然后封装回Service层的new出来的dish里面,然后才能让口味关联菜品ID。但是这里我们做的是修改操作,在接口文档中前端已经把菜品ID通过DishDTO传过来了 ,所有在浅拷贝那一步的时候我们的dish里面就已经有id值了,所以才可以直接遍历所有口味进行setDIshId的关联操作!最后再把口味批量插入到dish_flavor表 中即可。最后的最后,那就是更新操作涉及到两张表的增删改 操作,需要加事务注解@Transactional

(4)Mapper层

DishMapper的selectById方法,查询菜品基本信息。

DishFlavorMapper的selectByDishId方法,查询对应菜品的所有口味信息。

DishMapper的update方法,更新菜品的基本信息。

DishFlavorMapper的deleteById方法,清空对应菜品的所有口味信息。

DishFlavorMapper的insertBatch方法,批量插入对应菜品新的口味信息。

四、总结

难度逐渐上升,第一次在项目中使用了AOP技术实现了公共字段填充的增强,重温了切点表达式和切面类的编写。同时新增操作也加入了文件上传,再次使用阿里云提供的OSS文件上传云服务,重新熟悉了一遍SpringBoot导入第三方jar包的流程,感觉还是不熟练。通过自己亲身踩坑经历深刻理解了修改操作和新增操作的不同点,关于ID是否已知,通过@Options注解如何在新增操作中也能取到ID。还有批量删除插入操作的动态SQL,Mybatis自动封装条件与别名的使用,对分页插件的使用流程又熟悉了一些,重温#{}和${}两种占位符的区别。

相关推荐
摆烂z2 小时前
mysql通过binlog恢复数据
数据库·mysql
老邓计算机毕设2 小时前
SSM学期分析与学习行为分析系统c8322(程序+源码+数据库+调试部署+开发环境)带论文文档1万字以上,文末可获取,系统界面在最后面
数据库·学习·ssm 框架·学期分析·学习行为分析
学Linux的语莫2 小时前
python创建redis连接池
数据库·redis·缓存
运维有小邓@2 小时前
Log360 的可扩展架构(三):数据流管道
数据库·架构
醇氧2 小时前
【Windows】安装mysql8
数据库·windows·mysql
温暖小土2 小时前
ClickHouse vs Apache Doris:2026年实时OLAP数据库选型深度解析
数据库·数据仓库·clickhouse·apache
专注数据的痴汉3 小时前
「数据获取」全国民用运输机场吞吐量排名(2006-2024)
java·大数据·服务器·数据库·信息可视化
海边的椰子树3 小时前
非常方便的MySQL迁移数据ClickHouse工具
数据库·mysql·clickhouse·迁移
yongui478343 小时前
使用C#实现Excel实时读取并导入SQL数据库
数据库·c#·excel