一、前言
上一期我们学习了redis的基本使用,但是我们会发现,这样操纵缓存还是挺麻烦的,原因就是没有使用注解,是直接在类中写的逻辑,这样会造成代码维护困难,耦合度太高,所以这一期我们将接触一个新的缓存控制框架,并且用这个框架实现客户端的购物车功能。
二、 Spring cache
为了测试各个注解的作用,我们这里重新创建一个项目,单独说一下这个框架。
我们尽可能简化这个项目,去掉了Service层,直接用controller调用Mapper:
java
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;
java
@SpringBootApplication
public class CacheDemoApplication {
public static void main(String[] args) {
SpringApplication.run(CacheDemoApplication.class,args);
}
}
java
@RestController
@RequestMapping("/user")
public class UserController {
@Autowired
private UserMapper userMapper;
@PostMapping
public User save(@RequestBody User user){
userMapper.insert(user);
return user;
}
@DeleteMapping
public void deleteById(Long id){
userMapper.deleteById(id);
}
@DeleteMapping("/delAll")
public void deleteAll(){
userMapper.deleteAll();
}
@GetMapping
public User getById(Long id){
User user = userMapper.getById(id);
return user;
}
}
java
@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的方法上的。
java
@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,有几种表示方式,如下图,但是第一种是最容易维护的。
java
@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"
java
@GetMapping
@Cacheable(cacheNames = "userCache",key = "#id")//key的生成:userCache::id //有就直接调用缓存,没有就调用数据库,加到缓存
public User getById(Long id){
User user = userMapper.getById(id);
return user;
}
4.@CacheEvict
这个注解是用于清除缓存的,可以定向清理,也可以清除全部:
定向:
java
@DeleteMapping
@CacheEvict(cacheNames = "userCache",key = "#id")//key的生成:userCache::id
public void deleteById(Long id){
userMapper.deleteById(id);
}
全部:
java
@DeleteMapping("/delAll")
@CacheEvict(cacheNames = "userCache",allEntries = true)
public void deleteAll(){
userMapper.deleteAll();
}
三、购物车功能
回到苍穹外卖中去,我们将完成购物车的功能实现,购物车整体功能没有菜品套餐那么多,它不需要修改,只需要增加、查看、删除、清空(删除全部)。
但是它的业务逻辑挺复杂的,尤其是增加,首先,购物车是私人的,也就是说我们需要根据用户id来对这个id的购物车进行操作,这个肯定就会用到BaseContext了。其次,购物车中点套餐的 "+" 只需要增加数量,但是点菜品就需要指定规格,这个规格是用于选择口味的,所以我们会在这个表中添加冗余字段,自然的,查询操作比起菜品也会更加复杂。
1.添加购物车
(1)文档
老规矩,先分析文档,可以看到是传回请求体,这里面会传三个参数,所以需要查看的表还挺多的。

这里看看DTO的属性,不然后面很难搞懂。
java
@Data
public class ShoppingCartDTO implements Serializable {
private Long dishId;
private Long setmealId;
private String dishFlavor;
}
(2)Controller
这个Controller看着还是人畜无害的,没什么好讲的。
java
/**
* 添加购物车
* @param shoppingCartDTO
* @return
*/
@ApiOperation("添加购物车")
@PostMapping("/add")
public Result add(@RequestBody ShoppingCartDTO shoppingCartDTO){
log.info("添加购物车,商品信息为:{}",shoppingCartDTO);
shoppingCartService.addShoppingCart(shoppingCartDTO);
return Result.success();
}
(3)Service层
接口:
java
/**
* 添加购物车
* @param shoppingCartDTO
*/
void addShoppingCart(ShoppingCartDTO shoppingCartDTO);
实现类:这个就开始唬人了,我们慢慢来分析一下它的逻辑。
java
/**
* 添加购物车
* @param shoppingCartDTO
*/
@Override
public 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:
java
/**
* 动态条件查询
* @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);
映射文件:
XML
<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.查看购物车
这里就不看文档了,因为我们都已经实现了这个接口了。
java
/**
* 查询购物车
* @return
*/
@ApiOperation("查询购物车")
@GetMapping("/list")
public Result<List<ShoppingCart>> list(){
log.info("查询购物车");
List<ShoppingCart> list = shoppingCartService.showShoppingCart();
return Result.success(list);
}
java
/**
* 查询购物车
* @return
*/
List<ShoppingCart> showShoppingCart();
java
/**
* 查询购物车
* @return
*/
@Override
public List<ShoppingCart> showShoppingCart() {
//获取到当前微信用户的id
Long userId = BaseContext.getCurrentId();
ShoppingCart shoppingCart = ShoppingCart.builder().
userId(userId).
build();
List<ShoppingCart> list = shoppingCartMapper.list(shoppingCart);
return list;
}
3.清空购物车
这个功能也很简单,只需要根据用户id直接删除整条数据即可。
java
/**
* 清空购物车
* @return
*/
@ApiOperation("清空购物车")
@DeleteMapping("/clean")
public Result clean(){
shoppingCartService.cleanShoppingCart();
return Result.success();
}
java
/**
* 清空购物车
*/
@Override
public void cleanShoppingCart() {
Long userId = BaseContext.getCurrentId();
shoppingCartMapper.deleteByUserId(userId);
}
java
/**
* 清空购物车
*/
void cleanShoppingCart();
java
/**
* 根据用户Id删除购物车数据
* @param userId
*/
@Delete("delete from shopping_cart where user_id = #{userId}")
void deleteByUserId(Long userId);
4.删除购物车
下面代码是我自己写的了。
java
/**
* 删除一个商品
* @return
*/
@ApiOperation("删除一个商品")
@PostMapping("/sub")
public Result deleteShoppingCart(@RequestBody ShoppingCartDTO shoppingCartDTO){
log.info("删除一个商品:{}",shoppingCartDTO);
shoppingCartService.deleteShoppingCart(shoppingCartDTO);
return Result.success();
}
java
/**
* 删除商品
*/
void deleteShoppingCart(ShoppingCartDTO shoppingCartDTO);
java
/**
* 删除商品
*
* @param shoppingCartDTO
*/
@Override
public 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);
}
}
java
/**
* 删除一个商品
* @param shoppingCartId
*/
@Delete("delete from shopping_cart where id = #{shoppingCartId}")
void deleteShoppingCart(Long shoppingCartId);
花絮:bug
中间出了个bug,原因是搞忘绑定请求体了,导致无论按下哪个 "-" 号,删除的都是查询列表中的第一个商品(这个是因为如果没有绑定请求体,无论什么请求都将查询出所有在购物车中的商品,按照我的逻辑,删除list[0],就相当于删除了列表第一个数据),后面通过添加日志找出问题了,修改后即查询出的列表只会有一个商品了,这个时候删除列表第一个数据,就相当于删除指定的商品了。
在这里提醒各位,别忘了绑定请求体!!!