SpringBoot+MP针对复杂业务来手动封装一些涉及到多表操作的删除分页查询方法
Posted 懒羊羊.java
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了SpringBoot+MP针对复杂业务来手动封装一些涉及到多表操作的删除分页查询方法相关的知识,希望对你有一定的参考价值。
文章目录
前言
最近也是遇到了一些比较复杂的业务,MP内部提供的方法显然已经不能解决问题,针对场景需要自己手动封装一些方法来用,也是让自己明白了项目不单单都是简单的CRUD,涉及到多表还是比较复杂。
一.扩展MP提供的方法
场景一(删除)
在以前学习OOP中继承的时候,讲到在一组继承关系中,为了提高代码复用性,可扩展父类的方法。
大家都知道,MP的特色就是开发者不用写SQL,而这背后的原理是在一次又一次接口实现、类的继承中体现的(直接调用事先封装好的方法),那如果他提供的方法不能用于特定场景,是不是也可以在接口中扩展一下。
既然并不能帮助我们解决万难,那么在一些特殊的场景我们不能局限于使用它提供的方法,针对场景要进行适当“改装”。所以,当“时机成熟之时”我们可以扩展父类的方法,扩展功能
当实体间存在联系,也就是几张表之间相互关联。如果想要删除表中的一则信息我们肯定是要考虑他与其他实体的联系,比如:我删除一个菜类表中的一条数据,这个菜类下可能有菜品,也可能关联套餐,如果直接remove掉选中的菜类,那么我另两个实体中的数据就会丢失!
所以,我如果想要删除菜类的数据,直接通过MP提供的remove()方法显然是不够严谨全面的。在通过service调用remove()方法之前得加上贴合实际场景的判断条件。
这就要求我们对MP接口里的方法进行扩展:
怎么改进呢?反正层与层之间是继承、实现,那我在接口里重新定义一个remove()方法,不去用他给我提供的不就🆗了
类似于这样一个场景:
此时,前端已经向我们发来了请求:
在其实现类里,重写该方法,并加上针对“是否关联菜类,套餐”做一个逻辑判断,若不关联则直接调用父类的方法删除(直接删除无影响),反之则进入if()判断条件,抛出异常并终止删除操作!
怎么才能表示他有关联菜品,套餐呢?
直接根据相同的id查询就可,并返回查询到的行数是否大于0来作为判断条件
在实现类中,通过MP实现起来也就变得十分清晰明了,简简单单,就像这样:
/**
* 这个方法是我手动封装的!改装的!为了就是解决特殊情况
* 接收来自前端-接口-实现类的形参中ids参数并进行等值查询
* @param ids
*/
@Override
public void remove(Long ids)
//条件构造器
LambdaQueryWrapper<Dish> lqw1 = new LambdaQueryWrapper<>();
//添加查询条件
lqw1.eq(Dish::getCategoryId, ids);
//返回查询行数
int count1 = dishService.count(lqw1);
//查询当前分类是否关联了菜品,如果关联就抛出一个业务异常
if (count1 > 0)
//已将关联菜品,则抛出一个定义好的业务异常
throw new CustomException("菜类已关联彩菜品,无法删除");
LambdaQueryWrapper<Setmeal> lqw2 = new LambdaQueryWrapper<>();
lqw2.eq(Setmeal::getCategoryId, ids);
int count2 = setmealService.count(lqw2);
if (count2 > 0)
throw new CustomException("菜类已关联套餐,无法删除");
//如果不关联 则直接用框架的方法根据id删除它
super.removeById(ids);
上述的方法已经被我写到了实现类中,在Controller层里我们注入该类对应的接口即可使用自己扩展的remove()方法了!
二.多表操作与事务
场景二(保存)
同样地,在一个保存前端信息的场景中,由于前端的信息涉及到了两张表,我的实体不能一次性封装所有的数据,我需要扩展实体类来封装信息,而这就涉及到了多表的操作,也需要在Service层接口中扩展一个新的方法来处理两张表的信息。
首先定义一个新的DishDto实体类,通过继承获得了Dish的属性,为了能够保存DishFlavor表中的属性我在此类中做出如下扩展:
@Data
public class DishDto extends Dish
//用于数据传输 由于Dish中没有flavor属性,所以需要此类来扩展此类
private List<DishFlavor> flavors=new ArrayList<DishFlavor>(); //接收页面提交的flavor属性
经常使用MP的都知道,一般都是一张表对应一个实体类和一个Service,为了少写不必要的表,我直接在在形参里传入DishDto来封装前端的数据,然后在方法里操作DishDto中属于各自表的信息。
那么,在方法里是怎么操作多表的?
前端的信息已经被封装到了形参中的实体,为了将菜品的基本信息保存到dish表可以直接用Dish的Service对象来调用save()方法,为了保存DishFlavor表中的属性到dish_flavor表则是通过DishFlavor的service来调用saveBatch()保存flavor集合
@Override
@Transactional //由于涉及到多张表的操作,这里要开启事务
public void saveWithFlavor(DishDto dishDto)
//保存菜品基本信息到dish表
this.save(dishDto);//在这个类里我直接用this
Long dishId = dishDto.getId();//菜品id
//菜品口味
//由于少封装了dishId这里要遍历集合补充一下
List<DishFlavor> flavors = dishDto.getFlavors();
flavors=flavors.stream().map((temp)-> // stream流来遍历
temp.setDishId(dishId);
return temp;
).collect(Collectors.toList());
//保存菜品口味到dish_flavor,保存集合用saveBatch()方法,dishDto.getFlavors()得到口味集合
dishFlavorService.saveBatch(flavors);
有人可能会问,上面保存flavor字段的操作为什么比较复杂,甚至还需要遍历?
由于Java的单继承的机制,自定义的DTO类只继承了Dish类,而这就导致该类丢失了dishId属性。因为DishFlavor表中的属性的保存要和dishId相绑定存入表中,所以在方法里我们需要遍历一遍,给flavor设置dishId。
有人可能会问,你为什么不在DishDto里直接新定义一个属性dishId呢?
因为我无法给Long型数据指定泛型,而且也没有对应的继承关系,所以只能在方法中通过set方法获得DishFlavor的dishId。
写到这里,已经又一次地在MP的基础上扩展完了方法,而使用到该方法则是在Controller层中直接调用来操作封装前端信息的实体,就像这样:
@PostMapping()
public R<String> save(@RequestBody DishDto dishDto)
dishService.saveWithFlavor(dishDto);
return R.success("菜品信息保存成功!");
最后也是成功完成了保存:
场景三(修改)
在这样一个场景中:更新菜品信息的同时更新口味信息(两者不是一张表不能一次性更改)
为了实现这一功能,同样需要自己在接口里扩展方法
在实现类中,针对前端传来的实体类对应的数据,首先应更新菜品信息,也就是修改dish表,可以直接利用MP提供的updateById方法this.updateById(dishDto);
来修改
其次,对于DishFlavor表中的属性也就是对dish_flavor表的操作就稍微复杂,得先删除菜品中的口味,后给菜品设置新的口味信息:
(主要通过遍历,来动态绑定id赋予口味信息给菜品)
首先从DishDto里拿到用户选择的口味信息,我们通过.getFlavor()方法拿到口味信息value和name并封装到集合里,由于未与dishId绑定所以我们需要遍历一遍集合,并将DishDto里的dishId赋给集合里的元素
注: 两张表之间是通过dish表的id字段连接dish_flavor表中的dish_id,所以为了连接到,要将dish表的id动态地赋给dish_flavor表中的dish_id
@Override
@Transactional
public void updateWithFlavor(DishDto dishDto)
//根据id选择修改
this.updateById(dishDto);
//新建查询,查询口味信息
LambdaQueryWrapper<DishFlavor> dishDtoLambdaQueryWrapper = new LambdaQueryWrapper<>();
//找出当前菜品对应的口味信息
dishDtoLambdaQueryWrapper.eq(DishFlavor::getDishId, dishDto.getId());
//删除指定菜品的口味信息
dishFlavorService.remove(dishDtoLambdaQueryWrapper);
//从DishDto里拿到用户选择的口味信息
List<DishFlavor> flavors = dishDto.getFlavors();
//遍历一遍flavors赋给他新的id
flavors = flavors.stream().peek((temp) -> temp.setDishId(dishDto.getId())).collect(Collectors.toList());
//保存口味信息到dish_flavor表中
dishFlavorService.saveBatch(flavors);
最后,在Controller层中直接调用扩展的方法,实现功能:
初学者必看,SpringBoot+MybatisPlus+Swagger快速开发套路和总结
快速开发套路和总结
- 一、模块开发
- 快速开发套路
- 建module
- 改pom
- yml
- 主启动
- 二、业务类开发
- 业务类(手动)
- 建表写sql
- 实体类、dao、service
- controller
- 统一返回类
- 业务类(自动)
- MP插件
- 主键生成策略
- 自动填充详解
- 配置类
- 定义字段
- 测试自动填充
- 乐观锁
- 配置插件
- 在实体类的字段上加上`@Version`注解
- 逻辑删除
- 配置
- 实体类字段上加上`@TableLogic`注解
- 分页
- 配置类
- 性能分析
- 配置类
- 通用CRUD
- 配置类
- CRUD接口
- 三、Swagger
- 是什么?
- 有什么好处?
- 怎么用?
- 引入依赖
- springBoot整合swagger
- swagger的注解
- 使用swagger需要注意的问题
- 四、功能测试
- Swagger测试
- 删除功能测试
- 讲师分页功能
- 分页查询和多条件查询
- 添加讲师
- 查询讲师
- 修改讲师
- 异常处理
- 特定异常处理
- 自定义异常处理
- 统一日志处理
- 配置logback日志
- 将错误日志输出到文件
- 五、Bug记录
- userMapper爆红
- MP自动填充时,修改时间和创建时间同步更改
- 六、优秀文章参考
一、模块开发
Mybatis-Plus快速入门_小蜗牛耶的博客-博客_mybatis-plus
回顾可看我写的这篇文章
下文用boot代替springboot
mp代替mybatisplus
按照本文目录结构即可快速开发一套完整的CRUD接口,包括后面的Swagger测试
快速开发套路
这一套东西就是写接口对吧,所以写接口也就是写业务类,写业务类有个小口诀
- 建表写sql
- 定义实体类
- dao与mapper
- service和impl
- controller
那么我们用boot写微服务模块也有个小套路
- 建module
- 改pom
- 写yml
- 主启动
- 业务类
业务类对标我们上面的顺序
建module
建立新项目的时候可以用spring initializr初始化boot项目,也可以用maven的方式创建。这里我采用maven的方式来创建
改pom
pom出现的问题,跟网络环境有很大关系。
pom如果不断出错,可以更换网络或者删掉本地仓库中的包,重下
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>2.2.2.RELEASE</version>
<relativePath/> <!-- lookup parent from repository -->
</parent>
<groupId>com.caq</groupId>
<artifactId>mybatisplusdemo</artifactId>
<version>0.0.1-SNAPSHOT</version>
<name>mybatisplusdemo</name>
<description>Demo project for Spring Boot</description>
<properties>
<java.version>1.8</java.version>
</properties>
<dependencies>
<dependency>
<groupId>io.springfox</groupId>
<artifactId>springfox-swagger-ui</artifactId>
<scope>provided</scope>
<version>2.7.0</version>
</dependency>
<dependency>
<groupId>io.springfox</groupId>
<artifactId>springfox-swagger2</artifactId>
<version>2.7.0</version>
</dependency>
<dependency>
<groupId>com.baomidou</groupId>
<artifactId>mybatis-plus-boot-starter</artifactId>
<version>3.0.5</version>
</dependency>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<optional>true</optional>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter</artifactId>
</dependency>
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
<configuration>
<excludes>
<exclude>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
</exclude>
</excludes>
</configuration>
</plugin>
</plugins>
</build>
</project>
yml
这里我采用mysql8.0以上的驱动,配置url到时候?后面的值必须要加
驱动的名称和mysql5的也不一样记得区分
#端口号
server:
port: 8003
#服务名
spring:
application:
name: service-edu
#返回json的全局时间格式
jackson:
date-format: yyyy-MM-dd HH:mm:ss
time-zone: GMT+8
datasource:
username: root
password: root
url: jdbc:mysql://localhost:3306/mybatisplus?useSSL=false&useUnicode=true&characterEncoding=utf-8&serverTimezone=GMT%2B8
driver-class-name: com.mysql.cj.jdbc.Driver
profiles:
active: dev
# mybatis日志
mybatis-plus:
configuration:
log-impl: org.apache.ibatis.logging.stdout.StdOutImpl
主启动
package com.caq.mybatisplusdemo;
import org.mybatis.spring.annotation.MapperScan;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
@SpringBootApplication
public class MybatisplusdemoApplication
public static void main(String[] args)
SpringApplication.run(MybatisplusdemoApplication.class, args);
二、业务类开发
业务类(手动)
下面就开始写业务类了,业务类也遵守我们的步骤来写
建表写sql
CREATE TABLE user01
(
id int(20) NOT NULL COMMENT 主键ID ,
name01 VARCHAR(30) NULL DEFAULT NULL COMMENT 姓名,
age01 INT(11) NULL DEFAULT NULL COMMENT 年龄,
email01 VARCHAR(50) NULL DEFAULT NULL COMMENT 邮箱,
PRIMARY KEY (id)
);
DELETE FROM user01;
INSERT INTO user01 (id, name01, age01, email01) VALUES
(1, Jone, 18, test1@baomidou.com),
(2, Jack, 20, test2@baomidou.com),
(3, Tom, 28, test3@baomidou.com),
(4, Sandy, 21, test4@baomidou.com),
(5, Billie, 24, test5@baomidou.com);
实体类、dao、service
这里我们采用mp的插件,一键完成创建
安装插件
idea中连接数据库
右键你要一键生成实体类,dao,service的表格generator即可
生成说明
service接口说明
那么我们是不是要实现这么多方法呢?
当然不用,mp给我们定义好了一个IService的实现类,我们只需要实现类继承它并实现接口即可
controller
调用service,service调用mapper(dao)
开发controller,service,dao的过程就叫开发接口
为了前后端更好的沟通,我们可以定义一个统一的返回类
统一返回类
状态码定义
package com.caq.commonutils;
public interface ResultCode
//状态码:成功
public static Integer SUCCESS = 20000;
//状态码:失败
public static Integer ERROR = 20001;
统一返回类型R
package com.caq.commonutils;
import io.swagger.annotations.ApiModelProperty;
import lombok.Data;
import java.util.HashMap;
import java.util.Map;
/**
* 结果类
*/
@Data
public class R<T>
@ApiModelProperty(value = "是否成功")
private Boolean success;
@ApiModelProperty(value = "返回码")
private Integer code;
@ApiModelProperty(value = "返回消息")
private String message;
@ApiModelProperty(value = "返回数据")
private Map<String, Object> data = new HashMap<>();
//私有的构造器
private R()
//成功的静态方法
public static R ok()
R r = new R();
r.setSuccess(true);
r.setCode(ResultCode.SUCCESS);
r.setMessage("您的操作成功啦(*^▽^*)");
return r;
//失败的静态方法
public static R error()
R r = new R();
r.setSuccess(false);
r.setCode(ResultCode.ERROR);
r.setMessage("您的操作失败啦(⊙︿⊙)");
return r;
//the follow methods all return this,链式编程
public R success(Boolean success)
this.setSuccess(success);
return this;
public R code(Integer code)
this.setCode(code);
return this;
public R message(String message)
this.setMessage(message);
return this;
public R data(String key, Object value)
this.data.put(key, value);
return this;
public R data(Map<String, Object> map)
this.setData(map);
return this;
业务类(自动)
好,我们记住了这些固定的步骤之后其实还有个更简单的方式哦!
那就是mp里面的代码生成器!
下面直接放代码,因为它是固定的,我们只需要会更改即可
public class CodeGenerator
@Test
public void genCode()
// 1、创建代码生成器
AutoGenerator mpg = new AutoGenerator();
// 2、全局配置
GlobalConfig gc = new GlobalConfig();
String projectPath = System.getProperty("user.dir");
gc.setOutputDir(projectPath + "/src/main/java");
gc.setAuthor("Pyy");
gc.setOpen(false); //生成后是否打开资源管理器
gc.setServiceName("%sService"); //去掉Service接口的首字母I
gc.setIdType(IdType.AUTO); //主键策略
gc.setSwagger2(true);//开启Swagger2模式
mpg.setGlobalConfig(gc);
// 3、数据源配置
DataSourceConfig dsc = new DataSourceConfig();
dsc.setUrl("jdbc:mysql://localhost:3306/srb_core?serverTimezone=GMT%2B8&characterEncoding=utf-8");
dsc.setDriverName("com.mysql.cj.jdbc.Driver");
dsc.setUsername("root");
dsc.setPassword("root");
dsc.setDbType(DbType.MYSQL);
mpg.setDataSource(dsc);
// 4、包配置
PackageConfig pc = new PackageConfig();
pc.setParent("com.caq");
pc.setEntity("entity"); //此对象与数据库表结构一一对应,通过 DAO 层向上传输数据源对象。
mpg.setPackageInfo(pc);
// 5、策略配置
StrategyConfig strategy = new StrategyConfig();
strategy.setNaming(NamingStrategy.underline_to_camel);//数据库表映射到实体的命名策略
strategy.setColumnNaming(NamingStrategy.underline_to_camel);//数据库表字段映射到实体的命名策略
strategy.setEntityLombokModel(true); // lombok
strategy.setLogicDeleteFieldName("is_deleted");//逻辑删除字段名
strategy.setEntityBooleanColumnRemoveIsPrefix(true);//去掉布尔值的is_前缀(确保tinyint(1))
strategy.setRestControllerStyle(true); //restful api风格控制器
mpg.setStrategy(strategy);
// 6、执行
mpg.execute();
MP插件
业务类搞好后,为了让接口的功能更完善,我们最后加上mp自带的插件
https://baomidou.gitee.io/mybatis-plus-doc/#/performance-analysis-plugin
mp的2.x文档更详细一点
下面分别介绍主要的插件和一些常见知识点
主键生成策略
下面是我对实体类字段进行的设置,
这样设置的话我的主键在每次创建新用户的时候都会自动填充为分布式全局唯一ID 字符串类型
private static final long serialVersionUID = 1L;
@ApiModelProperty(value = "讲师ID")
/**
* 分布式应用时,我们需要生成分布式ID,可以选择使用@TableId(type=IdType.ID_WORKER),数据库中的主键为:
* IdType包括以下几类:
* AUTO : 数据库主键自增
* INPUT: 用户自行输入
* ID_WORKER: 分布式全局唯一ID, 长整型
* UUID: 32位UUID字符串
* NONE: 无状态
* ID_WORKER_STR: 分布式全局唯一ID 字符串类型
*/
@TableId(value = "id", type = IdType.ID_WORKER_STR)
private String id;
自动填充详解
自动填充一般应用在数据库创建时间或修改时间字段
[自动填充功能官网](自动填充功能 | MyBatis-Plus (baomidou.com))
配置类
package com.caq.servicebase.handle;
import com.baomidou.mybatisplus.core.handlers.MetaObjectHandler;
import org.apache.ibatis.reflection.MetaObject;
import org.springframework.stereotype.Component;
import java.util.Date;
@Component
public class MyMetaObjectHandler implements MetaObjectHandler
//插入的时候填充创建和修改字段
@Override
public void insertFill(MetaObject metaObject)
//属性名称,不是字段名称
this.setFieldValByName("gmtCreate",new Date(),metaObject);
this.setFieldValByName("gmtModified",new Date(),metaObject);
//修改的时候填充修改字段
@Override
public void updateFill(MetaObject metaObject)
this.setFieldValByName("gmtModified",new Date(),metaObject);
定义字段
@ApiModelProperty(value = "创建时间")
@TableField(fill= FieldFill.INSERT)
private Date gmtCreate;
@ApiModelProperty(value = "更新时间")
@TableField(fill=FieldFill.INSERT_UPDATE)
private Date gmtModified;
测试自动填充
乐观锁
锁是针对数据冲突的解决方案
悲观锁
正如其名,它指的是对数据被外界修改持保守(悲观),因此在整个数据处理过程中,将数据处于锁定状态。悲观锁的实现,往往依靠数据库提供的锁机制
乐观锁
相对悲观锁而言,乐观锁假设认为数据一般情况下不会有冲突,所以在数据进行提交更新的时候,才会正式对数据的冲突与否进行检测,如果发现了冲突,则让返回用户错误的信息,让用户决定如何去做。乐观锁的实现方式一般是记录数据版本
乐观锁的实现方式
- 取出记录,获取当前Version
- 更新时,带上这个version
- 执行更新时,set version = newVersion where version = oldVersion
- 如果version不对,就更新失败
乐观锁配置需要两步
配置插件
//乐观锁插件
@Bean
public OptimisticLockerInterceptor optimisticLockerInterceptor()
return new OptimisticLockerInterceptor();
在实体类的字段上加上@Version注解
@Version
private Integer version;
逻辑删除
只对自动注入的 sql 起效:
- 插入: 不作限制
- 查找: 追加 where 条件过滤掉已删除数据,且使用 wrapper.entity 生成的 where 条件会忽略该字段
- 更新: 追加 where 条件防止更新到已删除数据,且使用 wrapper.entity 生成的 where 条件会忽略该字段
- 删除: 转变为 更新
例如:
- 删除:
update user set deleted=1 where id = 1 and deleted=0
- 查找:
select id,name,deleted from user where deleted=0
字段类型支持说明:
- 支持所有数据类型(推荐使用
Integer
,Boolean
,LocalDateTime
)- 如果数据库字段使用
datetime
,逻辑未删除值和已删除值支持配置为字符串null
,另一个值支持配置为函数来获取值如now()
附录:
- 逻辑删除是为了方便数据恢复和保护数据本身价值等等的一种方案,但实际就是删除。
- 如果你需要频繁查出来看就不应使用逻辑删除,而是以一个状态去表示。
配置
@Bean
public ISqlInjector sqlInjector()
return new LogicSqlInjector();
实体类字段上加上@TableLogic注解
@TableLogic
private Integer deleted;
分页
- 自定义查询语句分页(自己写sql/mapper)
- spring 注入 mybatis 配置分页插件
配置类
//分页插件
@Bean
public PaginationInterceptor paginationInterceptor()
return new PaginationInterceptor();
性能分析
性能分析拦截器,用于输出每条 SQL 语句及其执行时间
注意!该插件只用于开发环境,不建议生产环境使用。。。
配置类
//性能分析插件
@Bean
@Profile("dev","test") //设置dev test环境开启,保证效率
public PerformanceInterceptor performanceInterceptor()
PerformanceInterceptor performanceInterceptor = new PerformanceInterceptor();
performanceInterceptor.setMaxTime(100);//设置sql执行的最大时间ms
performanceInterceptor.setFormat(true);
return performanceInterceptor;
通用CRUD
介绍完了mp的组件,要想使用只需要把它们写进一个配置类中让boot扫描即可
配置类
注入mp插件,完善接口功能
CRUD接口
package com.caq.mybatisplusdemo.controller;
@Api("crud测试")
@RestController
@RequestMapping("/testMP/user")
public class crudDemo
@Autowired
UserService userService;
@ApiOperation("增加用户")
@PostMapping("save")
public R saveUser(@RequestBody User user)
boolean save = userService.save(user);
if (save)
return R.ok();
else
return R.error();
@ApiOperation("查看所有用户")
@GetMapping("list")
public R listUser()
List<User> list = userService.list(null);
return R.ok().data("items",list);
@ApiOperation("查看某个用户")
@GetMapping("getByIdUser")
public R getByIdUser(@PathVariable String id)
User user = userService.getById(id);
return R.ok().data("user",user);
@ApiOperation("按ID删除user")
@DeleteMapping("delete")
public R removeUser(@ApiParam(name = "id",value = "讲师ID",required = true)@PathVariable String id)
boolean delete = userService.removeById(id);
if (delete)
return R.ok();
else
return R.error();
@ApiOperation("按ID更改user")
@PostMapping
public R updateUser(@RequestBody User user)
boolean update = userService.updateById(user);
if (update)
return R.ok();
else
return R.error();
三、Swagger
还是一样的套路,开局三连问
是什么?
一款接口测试工具
有什么好处?
对于后端开发人员来说
- 不用再手写WiKi接口拼大量的参数,避免手写错误
- 对代码侵入性低,采用全注解的方式,开发简单
- 方法参数名修改、增加、减少参数都可以直接生效,不用手动维护
- 缺点:增加了开发成本,写接口还得再写一套参数配置
对于前端开发来说
- 后端只需要定义好接口,会自动生成文档,接口功能、参数一目了然
- 联调方便,如果出问题,直接测试接口,实时检查参数和返回值,就可以快速定位是前端还是后端的问题
对于测试
- 对于某些没有前端界面UI的功能,可以用它来测试接口
- 操作简单,不用了解具体代码就可以操作
- 操作简单,不用了解具体代码就可以操作
怎么用?
引入swagger的依赖
目前推荐使用2.7.0版本,因为2.6.0版本有bug
引入依赖
<dependency>
<groupId>io.springfox</groupId>
<artifactId>springfox-swagger-ui</artifactId>
<scope>provided</scope>
<version>2.7.0</version>
</dependency>
<dependency>
<groupId>io.springfox</groupId>
<artifactId>springfox-swagger2</artifactId>
<version>2.7.0</version>
</dependency>
springBoot整合swagger
@Configuration
@MapperScan("com.caq.mybatisplusdemo.mapper")
@EnableSwagger2
public class MpConfig
@Bean
public Docket webApiConfig()
return new Docket(DocumentationType.SWAGGER_2)
.groupName("webApi")
.apiInfo(webApiInfo())
.select()
.paths(Predicates.not(PathSelectors.regex("/admin/.*")))
.paths(Predicates.not(PathSelectors.regex("/error.*")))
.build();
private ApiInfo webApiInfo()
return new ApiInfoBuilder()
.title("mp测试")
.description("本文档描述了mp接口定义")
.version("1.0")
.contact(new Contact("java", "http://java.com", "534215342@qq.com"))
.build();
swagger的注解
@Api():用在请求的类上,表示对类的说明,也代表了这个类是swagger2的资源
参数:
tags:说明该类的作用,参数是个数组,可以填多个。
value="该参数没什么意义,在UI界面上不显示,所以不用配置"
description = "用户基本信息操作"
@ApiOperation():用于方法,表示一个http请求访问该方法的操作
参数:
value="方法的用途和作用"
notes="方法的注意事项和备注"
tags:说明该方法的作用,参数是个数组,可以填多个。
格式:tags="作用1","作用2"
(在这里建议不使用这个参数,会使界面看上去有点乱,前两个常用)
@ApiModel():用于响应实体类上,用于说明实体作用
参数:
description="描述实体的作用"
@ApiModelProperty:用在属性上,描述实体类的属性
参数:
value="用户名" 描述参数的意义
name="name" 参数的变量名
required=true 参数是否必选
@ApiImplicitParams:用在请求的方法上,包含多@ApiImplicitParam
@ApiImplicitParam:用于方法,表示单独的请求参数
参数:
name="参数ming"
value="参数说明"
dataType="数据类型"
paramType="query" 表示参数放在哪里
· header 请求参数的获取:@RequestHeader
· query 请求参数的获取:@RequestParam
· path(用于restful接口) 请求参数的获取:@PathVariable
· body(不常用)
· form(不常用)
defaultValue="参数的默认值"
required="true" 表示参数是否必须传
@ApiParam():用于方法,参数,字段说明 表示对参数的要求和说明
参数:
name="参数名称"
value="参数的简要说明"
defaultValue="参数默认值"
required="true" 表示属性是否必填,默认为false
@ApiResponses:用于请求的方法上,根据响应码表示不同响应
一个@ApiResponses包含多个@ApiResponse
@ApiResponse:用在请求的方法上,表示不同的响应
参数:
code="404" 表示响应码(int型),可自定义
message="状态码对应的响应信息"
@ApiIgnore():用于类或者方法上,不被显示在页面上
@Profile(“dev”, “test”):用于配置类上,表示只对开发和测试环境有用
使用swagger需要注意的问题
- 对于只有一个HttpServletRequest参数的方法,如果参数小于5个,推荐使用 @ApiImplicitParams的方式单独封装每一个参数;如果参数大于5个,采用定义一个对象去封装所有参数的属性,然后使用@APiParam的方式
- 默认的访问地址:ip:port/swagger-ui.html#/,但是在shiro中,会拦截所有的请求,必须加上默认访问路径(比如项目中,就是ip:port/context/swagger-ui.html#/),然后登陆后才可以看到
- 在GET请求中,参数在Body体里面,不能使用@RequestBody。在POST请求,可以使用@RequestBody和@RequestParam,如果使用@RequestBody,对于参数转化的配置必须统一
- controller必须指定请求类型,否则swagger会把所有的类型(6种)都生成出来
- swagger在生产环境不能对外暴露,可以使用@Profile(“dev”, “prod”,“pre”)指定可以使用的环境
四、功能测试
下面我们就开始用Swagger来测试我们写的接口
Swagger测试
登录swaggerUI
ip:prot/swagger-ui.html
删除功能测试
逻辑删除我们是用Mp中的插件来实现的
所以在mp的配置类中添加逻辑删除插件即可
- 插入: 不作限制
- 查找: 追加 where 条件过滤掉已删除数据,且使用 wrapper.entity 生成的 where 条件会忽略该字段
- 更新: 追加 where 条件防止更新到已删除数据,且使用 wrapper.entity 生成的 where 条件会忽略该字段
- 删除: 转变为 更新
例如:
- 删除:
update user set deleted=1 where id = 1 and deleted=0
- 查找:
select id,name,deleted from user where deleted=0
在开发中,我们一般做逻辑删除
所谓逻辑删除不是真正的删除,而是在逻辑上删除不是在数据库中删除
步骤一
//逻辑删除插件
@Bean
public ISqlInjector sqlInjector()
return new LogicSqlInjector();
步骤二
实体类字段上加上@TableLogic
注解
@TableLogic
private Integer deleted;
讲师分页功能
分页功能我们也是用Mp中的插件来实现的
所以在mp的配置类中添加分页插件即可
步骤一
分页插件
//分页插件
@Bean
public PaginationInterceptor paginationInterceptor()
return new PaginationInterceptor();
步骤二
分页Controller方法
@ApiOperation("分页查询讲师功能")
@GetMapping("pageTeacher/current/limit")
public R pageTeacher(@PathVariable long current,
@PathVariable long limit)
//创建page对象
Page<EduTeacher> pageTeacher = new Page<>(current, limit);
//调用方法实现分页
//调用方法的时候,底层封装,把分页所有数据封装到pageTeacher对象里面
teacherService.page(pageTeacher, null);
long total = pageTeacher.getTotal();
List<EduTeacher> records = pageTeacher.getRecords();
Map<String, Object> map = new HashMap();
map.put("total", total);
map.put("rows", records);
return R.ok().data(map);
// 两种方式都可以
// return R.ok().data("total",total).data("rows",records);
分页查询和多条件查询
@requestbody注解的作用是使用json传递数据,并把json封装到对应对象里面
面试题补充:
你经常用springboot中的那些注解?
@RequestBody、@ResponseBody、@PathVariable
前者是以json格式传递数据
后者是返回json格式数据
步骤一
创建查询对象
package com.caq.eduservice.entity.vo;
import io.swagger.annotations.ApiModelProperty;
import lombok.Data;
@Data
public class TeacherQuery
// private Long current;
// private Long limit;
@ApiModelProperty(value = "教师名称,模糊查询")
private String name;
@ApiModelProperty(value = "头衔 1普通讲师 2高级讲师 3超级讲师")
private Integer level;
@ApiModelProperty(value = "查询开始时间", example = "2019-01-01 10:10:10")
private String begin;//注意,这里使用的是String类型,前端传过来的数据无需进行类型转换
// private Date begin;
@ApiModelProperty(value = "查询结束时间", example = "2019-12-01 10:10:10")
private String end;
// private Date end;
步骤二
分页查询和多条件查询接口
TeacherQuery的属性根据前端需要的查询条件来设置
@RequestBody(required = false)表示增加参数TeacherQuery teacherQuery,非必选
@ApiOperation("分页查询和多条件查询")
@PostMapping("pageTeacherCondition/current/limit")
public R pageTeacherCondition(@PathVariable long current,
@PathVariable long limit,
@RequestBody(required = false) TeacherQuery teacherQuery)
Page<EduTeacher> pageTeacher = new Page<>(current, limit);
//构建条件
QueryWrapper<EduTeacher> wrapper = new QueryWrapper<>();
// 多条件组合查询
Integer level = teacherQuery.getLevel();
String name = teacherQuery.getName();
String begin = teacherQuery.getBegin();
String end = teacherQuery.getEnd();
//判断条件值是否为空,如果不为空拼接条件
if (!StringUtils.isEmpty(name))
//构建条件
wrapper.like("name", name);
if (!StringUtils.isEmpty(level))
wrapper.eq("level", level);
if (!StringUtils.isEmpty(begin))
wrapper.ge("gmt_create", begin);
if (!StringUtils.isEmpty(end))
wrapper.eq("gmt_modified", end);
//排序,新创建的在后面
wrapper.orderByDesc("gmt_create");
// 调用方法实现条件查询分页
teacherService.page(pageTeacher, wrapper);
long total = pageTeacher.getTotal();
List<EduTeacher> records = pageTeacher.getRecords();
return R.ok().data("total", total).data("rows", records);
当然我们也可以写条件表达式的形式
service
public interface EduTeacherService extends IService<EduTeacher>
IPage<EduTeacher> pageList(Long current, Long limit, TeacherQuery teacherQuery);
impl
@Service
public class EduTeacherServiceImpl extends ServiceImpl<EduTeacherDao, EduTeacher> implements EduTeacherService
public IPage<EduTeacher> pageList(Long current, Long limit, TeacherQuery teacherQuery)
Integer level = teacherQuery.getLevel();
String name = teacherQuery.getName();
String begin = teacherQuery.getBegin();
String end = teacherQuery.getEnd();
LambdaQueryWrapper<EduTeacher> queryWrapper = new LambdaQueryWrapper<>();
queryWrapper.eq(level!=null, EduTeacher::getLevel,level)
.like(StringUtils.isNotBlank(name),EduTeacher::getName,name)
.ge(begin!=null,EduTeacher::getGmtCreate,begin)
.le(end!=null, EduTeacher::getGmtModified,end);
return this.page(new Page<>(current,limit), queryWrapper);
添加讲师
@PostMapping("addTeacher")
public R addTeacher(@RequestBody EduTeacher eduTeacher)
boolean save = teacherService.save(eduTeacher);
if (save)
return R.ok();
else
return R.error();
查询讲师
@GetMapping("getTeacher/id")
public R getTeacher(@PathVariable String id)
EduTeacher eduTeacher = teacherService.getById(id);
return R.ok().data("teacher",eduTeacher);
修改讲师
我们修改讲师用的传入的参数是一个讲师对象,讲师对象里必须有id,因为我们修改讲师用的是id
//讲师修改功能
@PostMapping("updateTeacher")
public R updateTeacher(@RequestBody EduTeacher eduTeacher)
boolean flag = teacherService.updateById(eduTeacher);
if (flag)
return R.ok();
else
return R.error();
异常处理
全局异常
//全局异常处理,当遇见Exception异常的时候调用error方法
@ExceptionHandler(Exception.class)
@ResponseBody//返回数据(它不在controller中所以要加上ResponseBody注解)
public R error(Exception e)
e.printStackTrace();
return R.error().message("执行了全局异常处理....");
特定异常处理
特定和全局异常怎么选择呢?
先找特定异常,如果没有则找全局异常
// 特定异常
@ExceptionHandler(ArithmeticException.class)
@ResponseBody//返回数据(它不在controller中所以要加上ResponseBody注解)
public R error(ArithmeticException e)
e.printStackTrace();
return R.error().message("执行了ArithmeticException异常处理....");
自定义异常处理
- 创建自定义异常类继承RuntimeException,写异常属性
- 在统一异常类添加规则
- 执行自定义异常
第一步、
@Data
@AllArgsConstructor
@NoArgsConstructor
public class Guliexception extends RuntimeException
private Integer code;//状态码
private String msg;//异常信息
第二步、
// 自定义异常处理
@ExceptionHandler(Guliexception.class)
@ResponseBody//返回数据(它不在controller中所以要加上ResponseBody注解)
public R error(Guliexception e)
e.printStackTrace();
//这一套链式调用记得多debug
return R.error().code(e.getCode()).message(e.getMsg());
第三步、
//模拟一个异常
try
int i = 10/0;
catch (Exception e)
/**执行自定义异常
* 传入的参数是自己写的异常类的构造方法的参数,这样能让代码更通用
*/
throw new Guliexception(20001,"执行了自定义异常处理.....");
测试
统一日志处理
spring boot内部使用Logback作为日志实现的框架。
Logback和log4j非常相似,如果你对log4j很熟悉,那对logback很快就会得心应手。
logback相对于log4j的一些优点
配置logback日志
删除 application.yml 中的日志配置
resources 中创建 logback-spring.xml
,名字固定的,不建议改
<?xml version="1.0" encoding="UTF-8"?>
<configuration scan="true" scanPeriod="10 seconds">
<!-- 日志级别从低到高分为TRACE < DEBUG < INFO < WARN < ERROR < FATAL,如果设
置为WARN,则低于WARN的信息都不会输出 -->
<!-- scan:当此属性设置为true时,配置文件如果发生改变,将会被重新加载,默认值
为true -->
<!-- scanPeriod:设置监测配置文件是否有修改的时间间隔,如果没有给出时间单位,默认
单位是毫秒。当scan为true时,此属性生效。默认的时间间隔为1分钟。 -->
<!-- debug:当此属性设置为true时,将打印出logback内部日志信息,实时查
看logback运行状态。默认值为false。 -->
<contextName>logback</contextName>
<!-- name的值是变量的名称,value的值时变量定义的值。通过定义的值会被插入
到logger上下文中。定义变量后,可以使“$”来使用变量。 -->
<!--日志输出在文件夹的哪个位置-->
<property name="log.path" value="D:\\JavaStudy\\gulixueyuan\\logback"/>
<!-- 彩色日志 -->
<!-- 配置格式变量:CONSOLE_LOG_PATTERN 彩色日志格式 -->
<!-- magenta:洋红 -->
<!-- boldMagenta:粗红-->
<!-- cyan:青色 -->
<!-- white:白色 -->
<!-- magenta:洋红 -->
<property name="CONSOLE_LOG_PATTERN"
value="%yellow(%dateyyyy-MM-dd HH:mm:ss) |%highlight(%-5level)
|%blue(%thread) |%blue(%file:%line) |%green(%logger) |%cyan(%msg%n)"/>
<!--输出到控制台-->
<appender name="CONSOLE" class="ch.qos.logback.core.ConsoleAppender">
<!--此日志appender是为开发使用,只配置最底级别,控制台输出的日志级别是大于或
等于此级别的日志信息-->
<!-- 例如:如果此处配置了INFO级别,则后面其他位置即使配置了DEBUG级别的日
志,也不会被输出 -->
<filter class="ch.qos.logback.classic.filter.ThresholdFilter">
<level>INFO</level>
</filter>
<encoder>
<Pattern>$CONSOLE_LOG_PATTERN</Pattern>
<!-- 设置字符集 -->
<charset>UTF-8</charset>
</encoder>
</appender>
<!--输出到文件-->
<!-- 时间滚动输出 level为 INFO 日志 -->
<appender name="INFO_FILE"
class="ch.qos.logback.core.rolling.RollingFileAppender">
<!-- 正在记录的日志文件的路径及文件名 -->
<file>$log.path/log_info.log</file>
<!--日志文件输出格式-->
<encoder>
<pattern>%dyyyy-MM-dd HH:mm:ss.SSS [%thread] %-5level
%logger50 - %msg%n
</pattern>
<charset>UTF-8</charset>
</encoder>
<!-- 日志记录器的滚动策略,按日期,按大小记录 -->
<rollingPolicy
class="ch.qos.logback.core.rolling.TimeBasedRollingPolicy">
<!-- 每天日志归档路径以及格式 -->
<fileNamePattern>$log.path/info/log-info-%dyyyy-MM-
dd.%i.log
</fileNamePattern>
<timeBasedFileNamingAndTriggeringPolicy
class="ch.qos.logback.core.rolling.SizeAndTim以上是关于SpringBoot+MP针对复杂业务来手动封装一些涉及到多表操作的删除分页查询方法的主要内容,如果未能解决你的问题,请参考以下文章
初学者必看,SpringBoot+MybatisPlus+Swagger快速开发套路和总结
SpringBoot项目的创建:手动创建一个Maven工程,然后引入SpringBoot所需的dependency来完成 (不需联网,但复杂)