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); }