三、day03学习
1、公共字段自动填充
公共字段即类似create_time、update_time、create_user、update_user在初始创建插入时,或者是在修改时都需要进行set设置,导致出现许多重复的冗余代码,当前模块的目的就是让这些公共字段进行自动的填充,而不是需要我们进行手动的set
1.1、自定义注解
通过使用自定义的注解,标识自动填充的方法,使这些方法会明确只有标了该自动注解的才需要自动填充
java
package com.sky.annotation;
import com.sky.enumeration.OperationType;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
// 自定义注解,用于标识需要自动填充的方法
@Target(ElementType.METHOD) // 该注解只能用于方法上
@Retention(RetentionPolicy.RUNTIME) // 该注解在运行时有效
public @interface AutoFill {
// 对于 create_time、update_time、create_user、update_user 这些公共字段,只有在 insert 和 update 方法中才需要自动填充
// 可使用一个枚举类来定义数据库的操作类型,即对数据库的哪些操作需要进行自动填充
OperationType value();
}
1.2、自定义切面类
java
package com.sky.aspect;
import com.sky.annotation.AutoFill;
import com.sky.constant.AutoFillConstant;
import com.sky.context.BaseContext;
import com.sky.enumeration.OperationType;
import org.aspectj.lang.JoinPoint;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Before;
import org.aspectj.lang.annotation.Pointcut;
import org.aspectj.lang.reflect.MethodSignature;
import org.springframework.stereotype.Component;
import java.lang.reflect.Method;
import java.time.LocalDateTime;
@Aspect
@Component
public class AutoFillAspect {
// 创建切入点,指定需要自动填充的方法
// 匹配 com.sky.mapper 包下的所有方法,且方法上有 AutoFill 注解
@Pointcut("execution(* com.sky.mapper.*.*(..)) && @annotation(com.sky.annotation.AutoFill)")
public void autoFillPointcut() {}
// 使用前置通知,在方法执行前自动填充
@Before("autoFillPointcut()")
public void autoFill(JoinPoint joinPoint) {
// 1、获取当前被拦截的方法上的数据库操作类型
// 从连接点中获取方法签名
MethodSignature methodSignature = (MethodSignature) joinPoint.getSignature();
// 获取该方法上的 AutoFill 注解
AutoFill autoFill = methodSignature.getMethod().getAnnotation(AutoFill.class);
// 获取 AutoFill 注解中的数据库操作类型
OperationType operationType = autoFill.value();
// 2、获取当前被拦截的方法的参数--即各个方法中的实体类
// 由于不同方法的实体类不同,所以采用 Object 数组接收方法的所有参数(注意:我们约定实体类作为参数时,必须放在第一个参数)
Object[] args = joinPoint.getArgs();
if(args == null||args.length == 0){
return;
}
// 3、准备要填充的数据,即要为 create_time、create_user、update_time、update_user 赋的值
LocalDateTime now = LocalDateTime.now();
Long currentId = BaseContext.getCurrentId();
// 4、根据数据库操作类型,为对应的属性赋值
if(operationType == OperationType.INSERT){
// 为实体类中的 create_time、create_user、update_time、update_user 赋值
// 由于我们约定实体类作为参数时,必须放在第一个参数,所以直接取第一个参数即可
Object entity = args[0];
try {
// 获取要赋值的参数的 set 方法
// 通过反射机制的 getMethod 方法获取要赋值的参数的 set 方法
// public Method getMethod(String name, Class<?>... parameterTypes) throws NoSuchMethodException, SecurityException
// --name:要获取的方法的名称
// --parameterTypes:要获取的方法的参数类型列表
Method setCreateTimeMethod = entity.getClass().getMethod(AutoFillConstant.SET_CREATE_TIME, LocalDateTime.class);
Method setCreateUserMethod = entity.getClass().getMethod(AutoFillConstant.SET_CREATE_USER, Long.class);
Method setUpdateTimeMethod = entity.getClass().getMethod(AutoFillConstant.SET_UPDATE_TIME, LocalDateTime.class);
Method setUpdateUserMethod = entity.getClass().getMethod(AutoFillConstant.SET_UPDATE_USER, Long.class);
// 调用 set 方法,为属性赋值,此处使用了反射机制
setCreateTimeMethod.invoke(entity, now);
setCreateUserMethod.invoke(entity, currentId);
setUpdateTimeMethod.invoke(entity, now);
setUpdateUserMethod.invoke(entity, currentId);
} catch (Exception e) {
e.printStackTrace();
}
}else if(operationType == OperationType.UPDATE){
// 为实体类中的 update_time、update_user 赋值
// 由于我们约定实体类作为参数时,必须放在第一个参数,所以直接取第一个参数即可
Object entity = args[0];
try {
// 获取要赋值的参数的 set 方法
Method setUpdateTimeMethod = entity.getClass().getMethod(AutoFillConstant.SET_UPDATE_TIME, LocalDateTime.class);
Method setUpdateUserMethod = entity.getClass().getMethod(AutoFillConstant.SET_UPDATE_USER, Long.class);
// 调用 set 方法,为属性赋值,此处使用了反射机制
setUpdateTimeMethod.invoke(entity, now);
setUpdateUserMethod.invoke(entity, currentId);
} catch (Exception e) {
e.printStackTrace();
}
}
}
}
1.3、为mapper添加注释
注意只展示了添加了自定义注解的方法,其他方法暂时隐藏
java
@Mapper
public interface CategoryMapper {
// 修改分类,直接使用动态更新
// 启用禁用分类,直接使用动态更新,将 status 字段更新为传入的 status 值
@AutoFill(OperationType.UPDATE)
void update(Category category);
// 新增分类
// 新增分类,由于只是单句 SQL 语句故使用注解而不是 xml 文件映射(已开启驼峰命名)
// id 由于是设置了自增,故无需传入
@Insert("insert into category(type, name, sort, status, create_time, update_time, create_user, update_user)" +
" values(#{type}, #{name}, #{sort}, #{status}, #{createTime}, #{updateTime}, #{createUser}, #{updateUser})")
@AutoFill(OperationType.INSERT)
void insert(Category category);
}
java
@Mapper
public interface EmployeeMapper {
// 新增员工,由于只是单句 SQL 语句故使用注解而不是 xml 文件映射(已开启驼峰命名)
// id 由于是设置了自增,故无需传入
@Insert("insert into employee(username, name, password, phone, sex, id_number, status, create_time, update_time, create_user, update_user)" +
" values(#{username}, #{name}, #{password}, #{phone}, #{sex}, #{idNumber}, #{status}, #{createTime}, #{updateTime}, #{createUser}, #{updateUser})")
// 使用自定义注解 AutoFill 自动填充 create_time, update_time, create_user, update_user 字段
// 数据库操作类型为 INSERT
// 当切面类扫描到该方法时,会自动调用切面类中的通知方法
@AutoFill(OperationType.INSERT)
void insert(Employee employee);
// 启用禁用员工账号,本质是根据 id 去数据库中修改员工的 status 属性
// 编辑员工信息,本质是根据 id 去数据库中修改员工的其他属性
// 使用自定义注解 AutoFill 自动填充 update_time, update_user 字段
// 数据库操作类型为 UPDATE
// 当切面类扫描到该方法时,会自动调用切面类中的通知方法
@AutoFill(OperationType.UPDATE)
void update(Employee employee);
}
1.4、去除手动赋值公共字段
即将service的实现类中的手动赋值去掉
1.5、测试及结果

注意如果你在调试时,发现你的create_id或者update_id是null,可能是因为拦截器没有生效,建议重新去检查拦截器
2、新增菜品
根据接口文档,该业务涉及三个部分:根据类型查询分类、文件上传、新增菜品
- 根据类型查询分类(已在day02中完成)
- 文件上传可使用本地存储或者使用阿里云oss(云存储服务)(未完成)
以阿里云oss为例,文件上传后通过其返回的图片的url地址返回给前端,以达到当用户上传图片后可以直接通过前端的data的url地址查看到图片
- 新增菜品(未完成)
2.1、文件上传
①、配置阿里云oss
Ⅰ、总配置文件
yaml
sky:
# 。。。。此处省略其他无关配置
# 配置阿里云的 oss
alioss:
# 由于我已经在系统环境中配置了 access-key-id 和 access-key-secret,故这里不需要明文配置
# access-key-id:
# access-key-secret:
# 当前是设置的是 dev 环境下的 yml,故需要在 application-dev.yml中进行以下的配置
endpoint: ${sky.alioss.endpoint}
bucket-name: ${sky.alioss.bucket-name}
Ⅱ、开发配置文件
yaml
sky:
datasource:
driver-class-name: com.mysql.cj.jdbc.Driver
host: localhost
port: 3306
database: sky_take_out
username: root
password: 123456
alioss:
# 注意要和你的阿里云 oss 的 bucket 名以及创建时设置的地域相同
# 如果没有在系统环境中配置 access-key-id 和 access-key-secret,这里需要额外添加这两项并明文设置
bucket-name: tbweb1
endpoint: oss-cn-shenzhen.aliyuncs.com
②、初始化工具类
java
@Component
// 该注解可以将配置文件中的属性绑定到 Bean 中
// 即在配置文件中的变量属性可映射到该类,与之对应
@ConfigurationProperties(prefix = "sky.alioss")
@Data
public class AliOssProperties {
// 原配置类
// private String endpoint;
// private String accessKeyId;
// private String accessKeySecret;
// private String bucketName;
// 由于我将 accesskey 的 id和 secret 配置在了系统的环境变量,所以此处为了可识别到采用环境变量注入的方法
private String endpoint;
@Value("${OSS_ACCESS_KEY_ID}")
private String accessKeyId;
@Value("${OSS_ACCESS_KEY_SECRET}")
private String accessKeySecret;
private String bucketName;
}
上面这一步是因为我并没有采用明文在配置文件中配置密钥的id和secret,所以需要采用系统环境变量注入的方法进行优化
java
@Configuration
@Slf4j
// 该配置类是为了给 OssUtil 工具类中的成员变量进行初始化赋值
public class OssConfiguration {
@Bean
@ConditionalOnMissingBean // 该注解是为了确保只有一个该工具类的 Bean 被创建
// 通过 AliOssProperties(与配置文件相映射)赋值返回一个 AliOssUtil 的工具类
public AliOssUtil aliOssUtil(AliOssProperties aliOssProperties) {
// 通过 log 记录创建 AliOssUtil 工具类的 Bean 时的参数,确保参数的正确性
log.info("开始创建 AliOssUtil 工具类的 Bean,参数为:{}", aliOssProperties);
return new AliOssUtil(aliOssProperties.getEndpoint()
,aliOssProperties.getAccessKeyId()
,aliOssProperties.getAccessKeySecret()
,aliOssProperties.getBucketName());
}
}
③、controller层测试
java
@RestController
@RequestMapping("/common")
@Slf4j
public class CommonController {
@Autowired
private AliOssUtil aliOssUtil;
@PostMapping("/upload")
// 通过接口文档可见,该方法的三项返回值中的 data(表示文件上传后的请求路径) 是 String 类型,且必须包含
// 注意传进去的是 MultipartFile file,该变量名要和前端的请求参数名一致
public Result<String> upload(MultipartFile file) {
try {
String originalFilename = file.getOriginalFilename();
// 建议使用 UUID+文件后缀名的方式来命名上传到 oss 的文件,避免文件名冲突
// 文件后缀名可通过对初始文件名的 "."截取获得
// 例如:"image.jpg" 的后缀名就是 ".jpg"
String objectName = UUID.randomUUID().toString() + originalFilename.substring(originalFilename.lastIndexOf("."));
// upload 方法返回的是文件的请求路径
String filePath = aliOssUtil.upload(file.getBytes(), objectName);
return Result.success(filePath);
} catch (IOException e) {
log.error("文件上传失败:{}", e.getMessage());
}
// 通过定义好的常量返回上传失败的信息
return Result.error(MessageConstant.UPLOAD_FAILED);
}
}
通过前后端联调的方式进行测试(当然也可以通过 Postman进行测试)

2.2、新增菜品
由接口文档可见,该功能的请求参数较多,我们可以通过将这些多个参数封装为一个DishDTO,其中的flavors又通过创建一个 DishFlavor类进行封装

①、controller层
java
@RestController
// 由于 nginx 的反向代理配置,已将初始路径设置为:http://localhost/api/ 或者也可以设置为 http://localhost:8080/
@RequestMapping("/dish")
@Slf4j
// 菜品相关接口
public class DishController {
@Autowired
private DishService dishService;
@PostMapping
// 新增菜品
// 根据接口文档可见将请求参数封装为 DishDTO 对象,故需要使用 @RequestBody 注解
// 请求参数默认是 @RequestParam(可写可不写)
public Result save(@RequestBody DishDTO dishDTO) {
log.info("新增菜品");
dishService.saveWithFlavor(dishDTO);
return Result.success();
}
}
②、service层
java
public interface DishService {
// 新增菜品和口味
void saveWithFlavor(DishDTO dishDTO);
}
java
@Service
public class DishServiceImpl implements DishService {
@Autowired
private DishMapper dishMapper;
@Autowired
private DishFlavorMapper dishFlavorMapper;
// 实现新增菜品和口味
// 本方法将对菜品表和菜品口味表进行新增操作,为保持数据一致性,需要在一个事务中完成(数据原子性)
@Transactional
public void saveWithFlavor(DishDTO dishDTO){
// 1、每次新增一条菜品数据
// 由于我们目前只需要对菜品表进行操作,而不需要对菜品口味表进行操作,所以直接创建一个新的 Dish 实体类
Dish dish = new Dish();
// 从 DTO 中复制属性到实体类
BeanUtils.copyProperties(dishDTO, dish);
dishMapper.insert(dish);
// 2、每次新增多条菜品口味数据
// 从 DTO 中获取菜品口味列表
List<DishFlavor> flavors = dishDTO.getFlavors();
// 为每个口味设置菜品 id
// 由于我们在 DishMapper.xml 中配置了主键自增,所以可以直接从实体类中获取 id
flavors.forEach(flavor -> flavor.setDishId(dish.getId()));
// 批量新增菜品口味数据
dishFlavorMapper.insertBatch(flavors);
}
}
③、mapper层
java
@Mapper
public interface DishMapper {
// 根据分类id查询菜品数量
@Select("select count(id) from dish where category_id = #{categoryId}")
Integer countByCategoryId(Long categoryId);
// 新增菜品,使用自定义的注解自动填充公共字段
@AutoFill(value = OperationType.INSERT)
void insert(Dish dish);
}
java
@Mapper
public interface DishFlavorMapper {
// 新增菜品口味数据
void insertBatch(List<DishFlavor> flavors);
}
xml
<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
"http://mybatis.org/dtd/mybatis-3-mapper.dtd" >
<mapper namespace="com.sky.mapper.DishMapper">
<!-- 新增菜品 -->
<!-- 主键 id 自增,所以不需要手动设置 -->
<!-- useGeneratedKeys="true"代表开启主键自增,keyProperty="id"代表将自增的主键值赋值给实体类的id属性 -->
<insert id="insert" useGeneratedKeys="true" keyProperty="id">
insert into dish (name, category_id, price, image, description, status, create_time, update_time, create_user, update_user)
values (#{name}, #{categoryId}, #{price}, #{image}, #{description}, #{status}, #{createTime}, #{updateTime}, #{createUser}, #{updateUser})
</insert>
</mapper>
xml
<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
"http://mybatis.org/dtd/mybatis-3-mapper.dtd" >
<mapper namespace="com.sky.mapper.DishFlavorMapper">
<!-- 新增菜品口味数据 -->
<insert id="insertBatch">
insert into dish_flavor (dish_id, name, value)
values
-- 取出集合 flavors 中的数据,使用 flavor变量进行接收,通过 foreach 循环批量插入,以逗号分隔
<foreach collection="flavors" item="flavor" separator=",">
(#{flavor.dishId}, #{flavor.name}, #{flavor.value})
</foreach>
</insert>
</mapper>
④、测试


3、菜品分页查询
由接口文档可见,该功能的请求参数较多,我们可以通过将这些多个参数封装为一个DishPageQueryDTO
同时,返回的data参数也较多,我们可以将data返回的多个参数封装为一个DishVO
请求参数复杂------>DTO
返回参数复杂------>VO(展示给前端的)
①、controller层
java
@RestController
// 由于 nginx 的反向代理配置,已将初始路径设置为:http://localhost/api/ 或者也可以设置为 http://localhost:8080/
@RequestMapping("/dish")
@Slf4j
// 菜品相关接口
public class DishController {
@Autowired
private DishService dishService;
@PostMapping
// 新增菜品
// 根据接口文档可见将请求参数(json格式)封装为 DishDTO 对象,故需要使用 @RequestBody 注解
// 请求参数默认是 @RequestParam(可写可不写)
public Result save(@RequestBody DishDTO dishDTO) {
log.info("新增菜品");
dishService.saveWithFlavor(dishDTO);
return Result.success();
}
@GetMapping("/page")
// 分页查询菜品
// 根据接口文档可将请求参数封装为一个 DishPageQueryDTO 对象
// 由于请求参数不是json格式,而是地址栏参数,所以不需要 @RequestBody 注解
public Result<PageResult> page(DishPageQueryDTO dishPageQueryDTO) {
log.info("分页查询菜品");
PageResult pageResult = dishService.pageQuery(dishPageQueryDTO);
return Result.success(pageResult);
}
}
②、service层
java
public interface DishService {
// 分页查询菜品
PageResult pageQuery(DishPageQueryDTO dishPageQueryDTO);
}
java
@Service
public class DishServiceImpl implements DishService {
@Autowired
private DishMapper dishMapper;
@Autowired
private DishFlavorMapper dishFlavorMapper;
// 实现分页查询菜品
public PageResult pageQuery(DishPageQueryDTO dishPageQueryDTO) {
// 1、使用 PageHelper 工具类进行分页查询
// 第一个参数为当前页码,第二个参数为每页显示记录数
PageHelper.startPage(dishPageQueryDTO.getPage(), dishPageQueryDTO.getPageSize());
// 2、调用 mapper 持久层去进行数据库的查询
// 此处使用 DishVO 而不是实体类和 DTO,是因为我们需要在分页查询结果中包含分类名称(这是接口文档中返回给前端的一个额外字段)
// 注意必须返回的是 Page<DishVO> 类型,而不是 List<DishVO> 类型,这是使用 PageHelper 工具类的要求
// 因为 PageHelper 工具类会自动将查询结果封装为 Page<DishVO> 类型
Page<DishVO> page = dishMapper.mypageQuery(dishPageQueryDTO);
// 3、将 page 分页查询结果包装为 PageResult 类型
// 第一个参数为总记录数,第二个参数为当前页记录列表
long total = page.getTotal();
List<DishVO> dishList = page.getResult();
// 4、返回 PageResult 类型的分页查询结果
return new PageResult(total, dishList);
}
}
③、mapper层
java
@Mapper
public interface DishMapper {
// 根据分类id查询菜品数量
@Select("select count(id) from dish where category_id = #{categoryId}")
Integer countByCategoryId(Long categoryId);
// 新增菜品,使用自定义的注解自动填充公共字段
@AutoFill(value = OperationType.INSERT)
void insert(Dish dish);
// 分页查询菜品
Page<DishVO> mypageQuery(DishPageQueryDTO dishPageQueryDTO);
}
xml
<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
"http://mybatis.org/dtd/mybatis-3-mapper.dtd" >
<mapper namespace="com.sky.mapper.DishMapper">
<!-- 新增菜品 -->
<!-- 主键 id 自增,所以不需要手动设置 -->
<!-- useGeneratedKeys="true"代表开启主键自增,keyProperty="id"代表将自增的主键值赋值给实体类的id属性 -->
<insert id="insert" useGeneratedKeys="true" keyProperty="id">
insert into dish (name, category_id, price, image, description, status, create_time, update_time, create_user, update_user)
values (#{name}, #{categoryId}, #{price}, #{image}, #{description}, #{status}, #{createTime}, #{updateTime}, #{createUser}, #{updateUser})
</insert>
<!-- 分页查询 -->
<!-- 输入的参数是 DishPageQueryDTO,返回的结果是 DishVO,用于前端展示 -->
<select id="mypageQuery" parameterType="com.sky.dto.DishPageQueryDTO" resultType="com.sky.vo.DishVO">
-- 将 dish 表和 category 表进行左连接,将 category 表中的 name 命名为 categoryName 字段
select d.*, c.name as categoryName
from dish d
left join category c on d.category_id = c.id
<where>
-- 当 name 不为空时,模糊查询含手动输入的 name 字段
<if test="name != null and name != ''"> and d.name like concat('%',#{name},'%') </if>
<if test="categoryId != null"> and d.category_id = #{categoryId} </if>
<if test="status != null"> and d.status = #{status} </if>
</where>
order by d.create_time desc
</select>
</mapper>
④、测试
由于我们的令牌在appropriate.yml中是有设置有效时长的,即超过设置的时间会过期,当在查询时报错401就是因为令牌过期,我们需要重新登录来获得新的有效的令牌

可见成功完成分页查询,同时如果是使用前后端联调也可以正常查看
4、删除菜品
由接口文档可见,该功能的请求参数较多,我们可以通过将这些多个参数封装为一个DishPageQueryDTO
同时,返回的data参数也较多,我们可以将data返回的多个参数封装为一个DishVO
①、controller层
java
@RestController
// 由于 nginx 的反向代理配置,已将初始路径设置为:http://localhost/api/ 或者也可以设置为 http://localhost:8080/
@RequestMapping("/dish")
@Slf4j
// 菜品相关接口
public class DishController {
@Autowired
private DishService dishService;
@DeleteMapping
// 菜品批量删除
public Result delete(@RequestParam List<Long> ids) {
dishService.deleteBatch(ids);
return Result.success();
}
}
②、service层
java
public interface DishService {
// 菜品批量删除
void deleteBatch(List<Long> ids);
}
java
@Service
public class DishServiceImpl implements DishService {
@Autowired
private DishMapper dishMapper;
@Autowired
private DishFlavorMapper dishFlavorMapper;
@Autowired
private SetmealDishMapper setmealDishMapper;
// 实现菜品批量删除
// 本方法是对多个表一起进行操作,需要确保数据一致性,所以需要在一个事务中完成(数据原子性)
// 如:删除菜品前需确保其是否包含在其他套餐中或是否在起售状态,否则会导致数据不一致;删除菜品后其对应的口味数据也要一起删除
// 具体删除规则应见接口文档
@Transactional
public void deleteBatch(List<Long> ids) {
// 1、判断是否在起售状态
for(Long id : ids) {
// 通过遍历获取到每个 id 对应的菜品实体类
Dish dish = dishMapper.getById(id);
if(dish.getStatus() == 1) {
// 菜品在起售状态,不能删除
// 抛出自定义的异常,同时通过定义的常量类传递异常信息
throw new DeletionNotAllowedException(MessageConstant.DISH_ON_SALE);
}
}
// 2、判断是否和其他套餐关联
// 通过传入的菜品 id 查询是否和其他套餐关联
List<Long > setmealIds = setmealDishMapper.getSetmealIdsByDishIds(ids);
if(setmealIds != null && setmealIds.size() > 0) {
// 菜品关联了其他套餐,不能删除,抛出自定义的异常,同时通过定义的常量类传递异常信息
throw new DeletionNotAllowedException(MessageConstant.DISH_BE_RELATED_BY_SETMEAL);
}
// 3、如果不在起售且不和套餐关联则删除菜品,同时删除其对应的口味数据
// for(long id : ids){
// dishMapper.deleteById(id);
// dishFlavorMapper.deleteBatchByDishIds(id);
// }
// 原版本每次删除均要进行遍历并且分别删除菜品和口味数据,效率较低
// 优化版本:直接批量删除菜品数据,同时通过关联关系删除其对应的口味数据
dishMapper.deleteBatch(ids);
dishFlavorMapper.deleteBatchByDishIds(ids);
}
}
③、mapper层
包括对DishMapper、DishFlavorMapper、SetmealDishMapper的设计
java
@Mapper
public interface DishMapper {
// 批量删除菜品
// @Delete("delete from dish where id in (#{id})")
// void deleteById(Long id);
// 优化版本
void deleteBatch(List<Long> ids);
}
xml
<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
"http://mybatis.org/dtd/mybatis-3-mapper.dtd" >
<mapper namespace="com.sky.mapper.DishMapper">
<!-- 批量删除菜品 -->
<delete id="deleteBatch">
delete from dish where id in
<foreach collection="ids" item="id" open="(" close=")" separator=",">
#{id}
</foreach>
</delete>
</mapper>
java
@Mapper
public interface DishFlavorMapper {
// 根据菜品id删除菜品口味数据
// @Delete("delete from dish_flavor where dish_id = #{dishId}")
// void deleteBatchByDishIds(Long dishId);
// 优化版本
void deleteBatchByDishIds(List<Long> dishIds);
}
xml
<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
"http://mybatis.org/dtd/mybatis-3-mapper.dtd" >
<mapper namespace="com.sky.mapper.DishFlavorMapper">
<!-- 根据菜品 id 批量删除菜品口味数据 -->
<delete id="deleteBatchByDishIds">
delete from dish_flavor where dish_id in
<foreach collection="dishIds" item="dishId" open="(" close=")" separator=",">
#{dishId}
</foreach>
</delete>
</mapper>
java
@Mapper
public interface SetmealDishMapper {
// 通过传入的菜品 id 查询是否和其他套餐关联
List<Long> getSetmealIdsByDishIds(List<Long> ids);
}
xml
<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
"http://mybatis.org/dtd/mybatis-3-mapper.dtd" >
<mapper namespace="com.sky.mapper.SetmealDishMapper">
<!-- 通过传入的菜品 id 查询是否和其他套餐关联 -->
<!-- 通过遍历 List<Long> ids,将其中的每个元素称作 id,以逗号分隔,(开头,)结尾,作为 SQL 语句的参数 -->
<select id="getSetmealIdsByDishIds" resultType="java.lang.Long">
select setmeal_id from setmeal_dish where dish_id in
<foreach collection="ids" item="id" open="(" close=")" separator=",">
#{id}
</foreach>
</select>
</mapper>
④、测试

5、修改菜品
由接口文档可见,该功能包含:根据id查询菜品(未完成)、根据分类查询菜品(已完成)、文件上传(已完成)、修改菜品(未完成)
- 根据id查询菜品:其返回的data应包含flavors集合------>故不是实体类Dish;还包含categoryName------>也不是DishDTO对象,与其返回的data数据相对应的是 DishVO对象
- 修改菜品:
5.1、根据id查询菜品
①、controller层
java
@RestController
// 由于 nginx 的反向代理配置,已将初始路径设置为:http://localhost/api/ 或者也可以设置为 http://localhost:8080/
@RequestMapping("/dish")
@Slf4j
// 菜品相关接口
public class DishController {
@Autowired
private DishService dishService;
@GetMapping("/{id}")
// 根据id查询菜品
// 由接口文档可见路径参数是 id,所以需要使用 @PathVariable 注解
// 参数名要与路径参数名一致,这样即可省略写成 @PathVariable Long id
public Result<DishVO> getById(@PathVariable("id") Long id) {
DishVO dishVO = dishService.getByIdWithFlavor(id);
return Result.success(dishVO);
}
}
②、service层
java
public interface DishService {
// 根据id查询菜品和口味
DishVO getByIdWithFlavor(Long id);
}
java
@Service
public class DishServiceImpl implements DishService {
@Autowired
private DishMapper dishMapper;
@Autowired
private DishFlavorMapper dishFlavorMapper;
@Autowired
private SetmealDishMapper setmealDishMapper;
// 实现根据id查询菜品和口味
// 本接口是对两个表进行查询操作,由于查询操作不涉及数据修改,所以不需要在一个事务中完成(数据一致性)
public DishVO getByIdWithFlavor(Long id) {
// 1、根据 id 查询菜品实体类
Dish dish = dishMapper.getById(id);
// 2、根据 id 查询菜品口味列表
List<DishFlavor> flavors = dishFlavorMapper.getByDishId(id);
// 3、将菜品实体类和口味列表封装为 DishVO 类型
DishVO dishVO = new DishVO();
BeanUtils.copyProperties(dish, dishVO);
// Dish 实体类中只包含除 flavors集合、categoryName 外的所有属性
// 所以需要手动设置 flavors 集合和 categoryName 属性,其中 categoryName 是从分类表中查询得到的,不需要额外设置
dishVO.setFlavors(flavors);
// 4、返回 DishVO 类型的查询结果
return dishVO;
}
}
③、mapper层
java
@Mapper
public interface DishFlavorMapper {
// 根据菜品id查询菜品口味数据
@Select("select * from dish_flavor where dish_id = #{dishId}")
List<DishFlavor> getByDishId(Long dishId);
}
④、测试

5.2、修改菜品
①、controller层
java
@RestController
// 由于 nginx 的反向代理配置,已将初始路径设置为:http://localhost/api/ 或者也可以设置为 http://localhost:8080/
@RequestMapping("/dish")
@Slf4j
// 菜品相关接口
public class DishController {
@Autowired
private DishService dishService;
@PutMapping
// 更新菜品以及其对应口味
// 根据接口文档可见将请求参数(json格式)封装为 DishDTO 对象,故需要使用 @RequestBody 注解
// 请求参数默认是 @RequestParam(可写可不写)
public Result update(@RequestBody DishDTO dishDTO) {
dishService.updateWithFlavor(dishDTO);
return Result.success();
}
}
②、service层
java
public interface DishService {
// 更新菜品和口味
void updateWithFlavor(DishDTO dishDTO);
}
java
@Service
public class DishServiceImpl implements DishService {
@Autowired
private DishMapper dishMapper;
@Autowired
private DishFlavorMapper dishFlavorMapper;
@Autowired
private SetmealDishMapper setmealDishMapper;
// 实现更新菜品和口味
// 本方法是对两个表进行操作,需要确保数据一致性,所以需要在一个事务中完成(数据原子性)
@Transactional
public void updateWithFlavor(DishDTO dishDTO) {
// 1、修改菜品实体类
Dish dish = new Dish();
BeanUtils.copyProperties(dishDTO, dish);
dishMapper.update(dish);
// 2、删除原有口味数据
// 通过使用 Collections.singletonList() 方法将 id 封装为一个元素的列表,
List<Long> dishIds = Collections.singletonList(dishDTO.getId());
dishFlavorMapper.deleteBatchByDishIds(dishIds);
// 3、批量新增新的口味数据
List<DishFlavor> flavors = dishDTO.getFlavors();
// 为每个口味数据设置菜品 id
if(flavors != null && !flavors.isEmpty()) {
// 由于我们在 DishMapper.xml 中配置了主键自增,所以可以直接从实体类中获取 id
flavors.forEach(flavor -> flavor.setDishId(dish.getId()));
}
dishFlavorMapper.insertBatch(flavors);
}
}
③、mapper层
java
@Mapper
public interface DishMapper {
// 更新菜品实体类
@AutoFill(value = OperationType.UPDATE)
void update(Dish dish);
}
xml
<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
"http://mybatis.org/dtd/mybatis-3-mapper.dtd" >
<mapper namespace="com.sky.mapper.DishMapper">
<!-- 更新菜品实体类 -->
<update id="update">
update dish
<set>
<if test="name != null"> name=#{name}, </if>
<if test="categoryId != null"> category_id=#{categoryId}, </if>
<if test="price != null"> price=#{price}, </if>
<if test="image != null"> image=#{image}, </if>
<if test="description != null"> description=#{description}, </if>
<if test="status != null"> status=#{status}, </if>
update_time=#{updateTime}, update_user=#{updateUser}
</set>
where id=#{id}
</update>
</mapper>
④、测试
