苍穹外卖 —— Spring Cache和购物车功能开发
一、前言
上一期我们学习了redis的基本使用,但是我们会发现,这样操纵缓存还是挺麻烦的,原因就是没有使用注解,是直接在类中写的逻辑,这样会造成代码维护困难,耦合度太高,所以这一期我们将接触一个新的缓存控制框架,并且用这个框架实现客户端的购物车功能。
二、 Spring cache
为了测试各个注解的作用,我们这里重新创建一个项目,单独说一下这个框架。
我们尽可能简化这个项目,去掉了Service层,直接用controller调用Mapper:
public class User implements Serializable {private static final long serialVersionUID = 1L;public int getAge() {return age;}public void setAge(int age) {this.age = age;}public Long getId() {return id;}public void setId(Long id) {this.id = id;}public String getName() {return name;}public void setName(String name) {this.name = name;}private Long id;private String name;private int age;
@SpringBootApplication
public class CacheDemoApplication {public static void main(String[] args) {SpringApplication.run(CacheDemoApplication.class,args);}
}
@RestController
@RequestMapping("/user")public class UserController {@Autowiredprivate UserMapper userMapper;@PostMappingpublic User save(@RequestBody User user){userMapper.insert(user);return user;}@DeleteMappingpublic void deleteById(Long id){userMapper.deleteById(id);}@DeleteMapping("/delAll")public void deleteAll(){userMapper.deleteAll();}@GetMappingpublic User getById(Long id){User user = userMapper.getById(id);return user;}}
@Mapper
public interface UserMapper{@Insert("insert into user(name,age) values (#{name},#{age})")@Options(useGeneratedKeys = true,keyProperty = "id")void insert(User user);@Delete("delete from user where id = #{id}")void deleteById(Long id);@Delete("delete from user")void deleteAll();@Select("select * from user where id = #{id}")User getById(Long id);
}
1.@EnableCaching
这个注解是用于开启缓存注解的,和@EnableTransactionManagement相似(这个是开启事务控制的),都是用在启动类上的注解,这个没什么好说的,后面的注解都是写在Controller的方法上的。
@SpringBootApplication
@EnableCaching//开启缓存注解
public class CacheDemoApplication {public static void main(String[] args) {SpringApplication.run(CacheDemoApplication.class,args);}
}2.@CachePut
这个注解是用于将方法返回值添加进缓存的注解。
参数:
1.cacheNames:是用于给缓存创建一个大目录并作为缓存的前缀名,如下图:

2.key:指定存入缓存的后缀名(通常是id),我们在这里会使用SpringEL(spring表达式),需要注意,我们传入的通常都是方法参数的id,有几种表示方式,如下图,但是第一种是最容易维护的。
@PostMapping@CachePut(cacheNames = "userCache",key = "#user.id") //荐,如果使用Spring cache缓存数据,key的生成:userCache::user.id,这里用springEL会将参数的值和key绑定,实现动态拼接//@CachePut(cacheNames = "userCache",key = "#result.id")//对象导航,但是这里的result是从返回值中取出来的//@CachePut(cacheNames = "userCache",key = "#p0.id")//表示第一个参数的id//@CachePut(cacheNames = "userCache",key = "#a0.id")//同上//@CachePut(cacheNames = "userCache",key = "#root.args[0].id")//同上public User save(@RequestBody User user){userMapper.insert(user);return user;}3.@Cacheable
这个注解是用于调用缓存的,如果有这个缓存就直接从缓存调用,如果没有,就从数据库调用,然后存到缓存中去。
这里会存在一个问题,就是如果缓存中没有,数据库中也没有,那还会被存到缓存中去吗?答案是会,但是存进去的是null(因为数据库中没有这个数据),这个现象叫做缓存污染,这样会导致缓存中出现无用信息,基于这个问题,@Cacheable会有一个参数:unless,加了这个参数就可以避免这种情况发生了:
unless = "#result == null"
@GetMapping@Cacheable(cacheNames = "userCache",key = "#id")//key的生成:userCache::id //有就直接调用缓存,没有就调用数据库,加到缓存public User getById(Long id){User user = userMapper.getById(id);return user;}4.@CacheEvict
这个注解是用于清除缓存的,可以定向清理,也可以清除全部:
定向:
@DeleteMapping@CacheEvict(cacheNames = "userCache",key = "#id")//key的生成:userCache::idpublic void deleteById(Long id){userMapper.deleteById(id);}全部:
@DeleteMapping("/delAll")@CacheEvict(cacheNames = "userCache",allEntries = true)public void deleteAll(){userMapper.deleteAll();}三、购物车功能
回到苍穹外卖中去,我们将完成购物车的功能实现,购物车整体功能没有菜品套餐那么多,它不需要修改,只需要增加、查看、删除、清空(删除全部)。
但是它的业务逻辑挺复杂的,尤其是增加,首先,购物车是私人的,也就是说我们需要根据用户id来对这个id的购物车进行操作,这个肯定就会用到BaseContext了。其次,购物车中点套餐的 “+” 只需要增加数量,但是点菜品就需要指定规格,这个规格是用于选择口味的,所以我们会在这个表中添加冗余字段,自然的,查询操作比起菜品也会更加复杂。
1.添加购物车
(1)文档
老规矩,先分析文档,可以看到是传回请求体,这里面会传三个参数,所以需要查看的表还挺多的。

这里看看DTO的属性,不然后面很难搞懂。
@Data
public class ShoppingCartDTO implements Serializable {private Long dishId;private Long setmealId;private String dishFlavor;}
(2)Controller
这个Controller看着还是人畜无害的,没什么好讲的。
/*** 添加购物车* @param shoppingCartDTO* @return*/@ApiOperation("添加购物车")@PostMapping("/add")public Result add(@RequestBody ShoppingCartDTO shoppingCartDTO){log.info("添加购物车,商品信息为:{}",shoppingCartDTO);shoppingCartService.addShoppingCart(shoppingCartDTO);return Result.success();}(3)Service层
接口:
/*** 添加购物车* @param shoppingCartDTO*/void addShoppingCart(ShoppingCartDTO shoppingCartDTO);实现类:这个就开始唬人了,我们慢慢来分析一下它的逻辑。
/*** 添加购物车* @param shoppingCartDTO*/@Overridepublic void addShoppingCart(ShoppingCartDTO shoppingCartDTO) {//判断当前加入到啊购物车中的商品是否已经存在了ShoppingCart shoppingCart = new ShoppingCart();BeanUtils.copyProperties(shoppingCartDTO,shoppingCart);Long userId = BaseContext.getCurrentId();shoppingCart.setUserId(userId);List<ShoppingCart> list = shoppingCartMapper.list(shoppingCart);//如果已经存在了,只需要将数量加一if(list != null && !list.isEmpty()){ShoppingCart cart = list.get(0);cart.setNumber(cart.getNumber() + 1);//update shopping_cart set number = ? where id = ?shoppingCartMapper.updateNumberById(cart);}else {//如果不存在,需要插入一条购物车数据//判断本次添加到购物车的是菜品还是套餐Long dishId = shoppingCartDTO.getDishId();if(dishId != null){//本次添加到购物车的是菜品Dish dish = dishMapper.getById(dishId);shoppingCart.setName(dish.getName());shoppingCart.setImage(dish.getImage());shoppingCart.setAmount(dish.getPrice());}else{//本次添加到购物车的是套餐Long setmealId = shoppingCartDTO.getSetmealId();Setmeal setmeal = setmealMapper.getById(setmealId);shoppingCart.setName(setmeal.getName());shoppingCart.setImage(setmeal.getImage());shoppingCart.setAmount(setmeal.getPrice());}//不管是套餐还是菜品都要设置下面两行shoppingCart.setNumber(1);shoppingCart.setCreateTime(LocalDateTime.now());shoppingCartMapper.insert(shoppingCart);}}分析逻辑:
1.判断当前加入到购物车中的商品是否已经存在了。
2.如果已经存在了,只需要将数量加一
3.如果不存在,需要插入一条购物车数据
4.判断本次添加到购物车的是菜品还是套餐
一个一个慢慢说:
第一,我们为什么要判断购物车的商品是否已经存在?这是因为同样的商品只需要将数量+1即可,不同的商品才需要将整个数据重新添加到购物车中。
第二,我们怎么判断是否存在?这个就需要调用查询方法了,而且这个查询方法需要很精确,不是说只根据id查询就行的,因为同一个菜品,口味不一样也不能算作一个商品,必须口味相同才能算一个菜品,而套餐就简单一些了,同一个id的套餐中的菜品肯定是一样的,这时候只需要比较id就行了。但是不管是套餐还是菜品,都是商品,我们不可能写两个Mapper,所以这里我们采用动态sql,对ShoppingCart中的每个相关属性都进行比较,即比较用户id、菜品id、套餐id、菜品口味id,如果能查出来,说明购物车里面就存在同一个商品了。
第三,为什么要判断本次添加到购物车的是菜品还是套餐?这是因为一个人一次只能将一个商品放入购物车,不可能同时将一个菜品和一个套餐放入,所以需要判断到底是菜品还是套餐,如果是菜品,就应该将ShoppingCart的菜品id改变,名称也应该改成菜品的名称,套餐同理,也就是说,每一个ShoppingCart的setmealId和dishId必定会空一个。
搞懂了这三条,基本上也就搞清楚添加购物车的逻辑了。
(4)持久层
Mapper:
/*** 动态条件查询* @param shoppingCart* @return*/List<ShoppingCart> list(ShoppingCart shoppingCart);/*** 插入购物车数据* @param shoppingCart*/@Insert("insert into shopping_cart(name, user_id, dish_id, setmeal_id, dish_flavor, amount, create_time) " +"VALUES (#{name},#{userId}, #{dishId} ,#{setmealId},#{dishFlavor},#{amount},#{createTime})")void insert(ShoppingCart shoppingCart);/*** 根据商品修改商品数量* @param shoppingCart*/@Update("update shopping_cart set number = #{number} where id = #{id}")void updateNumberById(ShoppingCart shoppingCart);映射文件:
<mapper namespace="com.sky.mapper.ShoppingCartMapper"><select id="list" resultType="com.sky.entity.ShoppingCart">select * from shopping_cart<where><if test="userId != null">and user_id = #{userId}</if><if test="setmealId != null">and setmeal_id = #{setmealId}</if><if test="dishId != null">and dish_id = #{dishId}</if><if test="dishFlavor != null">and dish_flavor = #{dishFlavor}</if></where></select>
</mapper>可以看出这确实很复杂,一个添加功能需要调用三种Mapper,这个是前面的所有功能都没有用过的,是目前最复杂的一个功能,但是好消息是,因为我们都写了三个Mapper了,所以后面的查询功能就会简单很多了。
2.查看购物车
这里就不看文档了,因为我们都已经实现了这个接口了。
/*** 查询购物车* @return*/@ApiOperation("查询购物车")@GetMapping("/list")public Result<List<ShoppingCart>> list(){log.info("查询购物车");List<ShoppingCart> list = shoppingCartService.showShoppingCart();return Result.success(list);}
/*** 查询购物车* @return*/List<ShoppingCart> showShoppingCart();
/*** 查询购物车* @return*/@Overridepublic List<ShoppingCart> showShoppingCart() {//获取到当前微信用户的idLong userId = BaseContext.getCurrentId();ShoppingCart shoppingCart = ShoppingCart.builder().userId(userId).build();List<ShoppingCart> list = shoppingCartMapper.list(shoppingCart);return list;}3.清空购物车
这个功能也很简单,只需要根据用户id直接删除整条数据即可。
/*** 清空购物车* @return*/@ApiOperation("清空购物车")@DeleteMapping("/clean")public Result clean(){shoppingCartService.cleanShoppingCart();return Result.success();}
/*** 清空购物车*/@Overridepublic void cleanShoppingCart() {Long userId = BaseContext.getCurrentId();shoppingCartMapper.deleteByUserId(userId);}
/*** 清空购物车*/void cleanShoppingCart();
/*** 根据用户Id删除购物车数据* @param userId*/@Delete("delete from shopping_cart where user_id = #{userId}")void deleteByUserId(Long userId);4.删除购物车
下面代码是我自己写的了。
/*** 删除一个商品* @return*/@ApiOperation("删除一个商品")@PostMapping("/sub")public Result deleteShoppingCart(@RequestBody ShoppingCartDTO shoppingCartDTO){log.info("删除一个商品:{}",shoppingCartDTO);shoppingCartService.deleteShoppingCart(shoppingCartDTO);return Result.success();}
/*** 删除商品*/void deleteShoppingCart(ShoppingCartDTO shoppingCartDTO);
/*** 删除商品** @param shoppingCartDTO*/@Overridepublic void deleteShoppingCart(ShoppingCartDTO shoppingCartDTO) {//判断当前加入到购物车中的商品是否已经存在了ShoppingCart shoppingCart = new ShoppingCart();BeanUtils.copyProperties(shoppingCartDTO, shoppingCart);Long userId = BaseContext.getCurrentId();shoppingCart.setUserId(userId);List<ShoppingCart> list = shoppingCartMapper.list(shoppingCart);log.info("获取的list,{}",list);if (list != null && !list.isEmpty()) {ShoppingCart cart = list.get(0);log.info("获取的cart,{}",cart);if (cart.getNumber() - 1 == 0) {shoppingCartMapper.deleteShoppingCart(cart.getId());}cart.setNumber(cart.getNumber() - 1);shoppingCartMapper.updateNumberById(cart);}else {throw new ShoppingCartBusinessException(MessageConstant.SHOPPING_CART_IS_NULL);}}
/*** 删除一个商品* @param shoppingCartId*/@Delete("delete from shopping_cart where id = #{shoppingCartId}")void deleteShoppingCart(Long shoppingCartId);花絮:bug
中间出了个bug,原因是搞忘绑定请求体了,导致无论按下哪个 ”-“ 号,删除的都是查询列表中的第一个商品(这个是因为如果没有绑定请求体,无论什么请求都将查询出所有在购物车中的商品,按照我的逻辑,删除list[0],就相当于删除了列表第一个数据),后面通过添加日志找出问题了,修改后即查询出的列表只会有一个商品了,这个时候删除列表第一个数据,就相当于删除指定的商品了。
在这里提醒各位,别忘了绑定请求体!!!
