有关SpringBoot项目的搭建在上一篇文章中已经有过详细说明,有需要的小伙伴请查看:从零开始SpringBoot项目搭建(一)
接上文,搭建好最基础的SpringBoot项目后。为了方便开发,我们需要:
swagger文档
用于接口文档自动生成。MybatisPlus
配置类,扩展自定义mapper方法
MybatisPlus
全局处理:新增数据、更新数据相关字段自动填充以及逻辑删除标记自动处理swagger
是比较流行的实时生成接口文档工具,这里我使用swagger2
版本,并使用knife4j
对接口文档进行美化加强。在前后端分离开发模式中,接口文档起到了不可忽视的作用,它相当于是前后端的约定协议,前端会根据后端出的接口文档进行开发。开发完成后,前后端再进行联调测试。
在pom.xml
文件中导入相关依赖:swagger2
以及knife4j
进行增强,请注意两者之间的版本兼容问题。一般swagger2.x
的话,knife4j
版本为2.0.0
都会兼容,记得在swagger
的两个依赖中都要排除swagger-models
和swagger-annotations
,否则会造成依赖冲突。
<properties>
<springfox-swagger2.version>2.9.2springfox-swagger2.version>
<springfox-swagger-ui.version>2.9.2springfox-swagger-ui.version>
<knife4j-spring-boot-starter.version>2.0.0knife4j-spring-boot-starter.version>
properties>
<dependency>
<groupId>io.springfoxgroupId>
<artifactId>springfox-swagger2artifactId>
<exclusions>
<exclusion>
<groupId>io.swaggergroupId>
<artifactId>swagger-modelsartifactId>
exclusion>
<exclusion>
<groupId>io.swaggergroupId>
<artifactId>swagger-annotationsartifactId>
exclusion>
exclusions>
<version>${springfox-swagger2.version}version>
dependency>
<dependency>
<groupId>io.springfoxgroupId>
<artifactId>springfox-swagger-uiartifactId>
<exclusions>
<exclusion>
<groupId>io.swaggergroupId>
<artifactId>swagger-modelsartifactId>
exclusion>
<exclusion>
<groupId>io.swaggergroupId>
<artifactId>swagger-annotationsartifactId>
exclusion>
exclusions>
<version>${springfox-swagger-ui.version}version>
dependency>
<dependency>
<groupId>com.github.xiaoymingroupId>
<artifactId>knife4j-spring-boot-starterartifactId>
<version>${knife4j-spring-boot-starter.version}version>
dependency>
application.properties
配置文件中添加一条配置参数用于控制是否生成swagger
文档:### swagger2
swagger.enable=true
@Configuration
、@EnableSwagger2
、@EnableKnife4j
请注意basePackage
中是扫描controller请求路径
的包所在位置,这里指定到core
就好;指定PathSelectors.any()
表示所有路径,这样core
下的所有controller
请求路径都会被扫描到并生成接口文档。
package com.study.core.config;
import com.github.xiaoymin.knife4j.spring.annotations.EnableKnife4j;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import springfox.documentation.builders.ApiInfoBuilder;
import springfox.documentation.builders.PathSelectors;
import springfox.documentation.builders.RequestHandlerSelectors;
import springfox.documentation.service.ApiInfo;
import springfox.documentation.spi.DocumentationType;
import springfox.documentation.spring.web.plugins.Docket;
import springfox.documentation.swagger2.annotations.EnableSwagger2;
@Configuration
@EnableSwagger2
@EnableKnife4j
public class Knife4jConfiguration {
@Value("${swagger.enable}")
private Boolean swaggerEnable;
@Bean
public Docket docket() {
return new Docket(DocumentationType.SWAGGER_2).apiInfo(apiInfo())
.enable(swaggerEnable)
.select()
.apis(RequestHandlerSelectors.basePackage("com.study.core"))
.paths(PathSelectors.any())
.build();
}
@Bean
public ApiInfo apiInfo() {
return new ApiInfoBuilder().title("测试平台-测试平台接口文档")
.version("v1.0.0")
.description("测试平台接口文档")
.build();
}
}
配置完成,启动项目。在浏览器输入项目地址:http://localhost:8080/doc.html
就能访问接口文档。如下图所示:
@Api
:常用于Controller
层,标记在类声明上,表示一个功能模块。使用属性tags
为功能添加模块名称以及描述信息。@Api(tags = "xx模块-xx接口")
@ApiOperation
:常用于Controller
层,标记在方法声明上,表示一个接口。使用属性value
为接口添加接口名称。@ApiOperation(value = "接口名")
@ApiModelProperty
:常用于请求和响应的实体类中,表示传输对象中的一个字段。使用属性value
为字段添加名称等信息。@ApiModelProperty(value = "字段名")
通常情况下,接口返回的数据结构分三个部分:状态码
、返回信息
、返回数据
。
package com.study.core.modular.common.enums;
import lombok.AllArgsConstructor;
import lombok.Getter;
@Getter
@AllArgsConstructor
public enum HttpResultCode {
//0~999
SUCCESS(0, "成功"),
SYSTEM_ERROR(999, "系统错误"),
//1000~1999 表示参数错误
PARAM_VALIDATED_FAILED(1000, "参数校验失败"),
PARAM_EXCEPTION(1001, "参数异常"),
TOKEN_VALIDATED_FAILED(1002, "Token校验失败"),
TOKEN_EXPIRED(1003, "Token已过期"),
//2000~2999 表示用户错误
USER_ACCOUNT_OR_PASSWORD_VALIDATE_FAILED(2000, "用户账号或密码不正确"),
USER_ACCOUNT_NOT_EXIST(2001, "用户账号不存在"),
//3000~3999 表示基础业务错误
DATA_NOT_EXISTS(3000, "数据不存在"),
DATA_EXISTED(3001, "数据已存在"),
BIZ_EXCEPTION(3002, "业务异常"),
BIZ_DATA_EXCEPTION(3003, "业务数据异常")
;
private final Integer code;
private final String message;
}
需要注意的是,我们需要对该实体类进行序列化操作:实现Serializable
接口。在完成该类的所有工作之后,不要忘了最后一步需要使其生成serialVersionUID
。可以在IDEA中安装插件GenerateSerialVersionUID
进行自动生成。
package com.study.core.modular.common.model;
import com.study.core.modular.common.enums.HttpResultCode;
import io.swagger.annotations.ApiModelProperty;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Data;
import lombok.NoArgsConstructor;
import java.io.Serializable;
@Data
@AllArgsConstructor
@NoArgsConstructor
@Builder
public class HttpResult<T> implements Serializable {
private static final long serialVersionUID = 9155082035856162703L;
@ApiModelProperty(value = "状态码")
private Integer code;
@ApiModelProperty(value = "信息")
private String message;
@ApiModelProperty(value = "数据")
private T data;
public static <T> HttpResult<T> success() {
return HttpResult.<T>builder()
.code(HttpResultCode.SUCCESS.getCode())
.message(HttpResultCode.SUCCESS.getMessage())
.build();
}
public static <T> HttpResult<T> success(T data) {
return HttpResult.<T>builder()
.code(HttpResultCode.SUCCESS.getCode())
.message(HttpResultCode.SUCCESS.getMessage())
.data(data)
.build();
}
public static <T> HttpResult<T> failure(HttpResultCode httpResultCode) {
return HttpResult.<T>builder()
.code(httpResultCode.getCode())
.message(httpResultCode.getMessage())
.build();
}
public static <T> HttpResult<T> failure(HttpResultCode httpResultCode, String message) {
return HttpResult.<T>builder()
.code(httpResultCode.getCode())
.message(message)
.build();
}
}
根据状态码枚举自定义异常类,继承运行时异常RuntimeException
,通样需要序列化,不要忘了最后生成serialVersionUID
package com.study.core.exception;
import com.study.core.modular.common.enums.HttpResultCode;
import lombok.Data;
@Data
public class BizException extends RuntimeException {
private static final long serialVersionUID = 6169725979353182781L;
private HttpResultCode httpResultCode;
public BizException(HttpResultCode httpResultCode) {
super(httpResultCode.getMessage());
this.httpResultCode = httpResultCode;
}
public BizException(HttpResultCode httpResultCode, String message) {
super(message);
this.httpResultCode = httpResultCode;
}
}
Hutool
工具类库相关Hutool工具类
。此工具类库的妙用很多,在整个项目都会大量使用该类库中的方法。涵盖了各种工具方法:时间工具类DateUtil
、字符串工具类StrUtil
、集合工具类CollUtil
、数组工具类ArrayUtil
…具体的内容小伙伴们自行搜索相关文档进行学习,Maven坐标
如下:<dependencies>
<dependency>
<groupId>cn.hutoolgroupId>
<artifactId>hutool-allartifactId>
<version>${hutool-all.version}version>
dependency>
dependencies>
springframwork
中的BeanUtils
进行了二次封装,将两个实体类相同名称属性的值进行复制转换:
null
则会被忽略以下是具体实现,定义为JavaBeanUtils
:
package com.study.core.util;
import cn.hutool.core.collection.CollUtil;
import cn.hutool.core.util.ArrayUtil;
import com.study.core.exception.BizException;
import com.study.core.modular.common.enums.HttpResultCode;
import org.springframework.beans.BeanUtils;
import org.springframework.beans.BeanWrapper;
import org.springframework.beans.BeanWrapperImpl;
import java.beans.PropertyDescriptor;
import java.util.Collections;
import java.util.List;
import java.util.Set;
public class JavaBeanUtils {
public static <T> T map(Object source, Class<T> target, String... ignoreProperties) {
try {
T result = target.newInstance();
copyNonNullProperties(source, result, ignoreProperties);
return result;
} catch (Exception e) {
throw new BizException(HttpResultCode.BIZ_EXCEPTION, "对象转换异常!");
}
}
public static void map(Object source, Object target, String... ignoreProperties) {
copyNonNullProperties(source, target, ignoreProperties);
}
public static <T> List<T> mapList(List<?> sourceList, Class<T> target) {
if (CollUtil.isEmpty(sourceList)) {
return Collections.emptyList();
}
List<T> resultList = CollUtil.newArrayList();
sourceList.forEach(src -> resultList.add(map(src, target)));
return resultList;
}
private static void copyNonNullProperties(Object source, Object target, String... ignoreProperties) {
BeanUtils.copyProperties(source, target, ArrayUtil.addAll(ignoreProperties, getNullPropertyNames(source)));
}
private static String[] getNullPropertyNames(Object source) {
final BeanWrapper src = new BeanWrapperImpl(source);
PropertyDescriptor[] pds = src.getPropertyDescriptors();
Set<String> emptyNames = CollUtil.newHashSet();
for (PropertyDescriptor pd : pds) {
Object srcPropertyValue = src.getPropertyValue(pd.getName());
if (srcPropertyValue == null) {
emptyNames.add(pd.getName());
}
}
return ArrayUtil.toArray(emptyNames, String.class);
}
}
一般情况下我们不会直接将数据库中的实体类返回给前端,通常会返回一个封装过的响应实体类。service
层是核心业务代码,处理后返回实体类(或实体类集合)后,再在controller
层中对返回的结果进行处理,转换并返回对应的响应实体类(或响应实体类集合)。
此方式其实还挺契合MybatisPlus
的,MybatisPlus
是单表查询,数据库中需要构建好各表之间的关联关系。service
层只查单表,后续返回的数据可在controller
层中根据外键id
去反查对应的关系表,然后填充与外键id
有关系的值为null
的数据。
在相应模块的model
包下新建一个response
包,所有响应实体类都放置在此包中,命名与业务相关,通常以xxxResponse
结尾。如下,创建一个DemoResponse
package com.study.core.modular.demo.model.response;
import com.fasterxml.jackson.annotation.JsonFormat;
import io.swagger.annotations.ApiModelProperty;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Data;
import lombok.NoArgsConstructor;
import java.util.Date;
@Data
@AllArgsConstructor
@NoArgsConstructor
@Builder
public class DemoResponse {
@ApiModelProperty(value = "主键id")
private String id;
@ApiModelProperty(value = "名称")
private String name;
@ApiModelProperty(value = "手机号")
private String phone;
@ApiModelProperty(value = "创建时间")
@JsonFormat(timezone = "GMT+8", pattern = "yyyy-MM-dd HH:mm:ss")
private Date createTime;
@ApiModelProperty(value = "更新时间")
@JsonFormat(timezone = "GMT+8", pattern = "yyyy-MM-dd HH:mm:ss")
private Date updateTime;
}
package com.study.core.modular.demo.service;
import cn.hutool.core.util.ObjectUtil;
import com.study.core.exception.BizException;
import com.study.core.modular.common.enums.HttpResultCode;
import com.study.core.modular.demo.dao.DemoMapper;
import com.study.core.modular.demo.model.entity.Demo;
import lombok.AllArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Service;
@Service
@AllArgsConstructor
@Slf4j
public class DemoService {
private final DemoMapper demoMapper;
public Demo get(String id) {
Demo demo = demoMapper.selectById(id);
if (ObjectUtil.isNull(demo)) {
throw new BizException(HttpResultCode.DATA_NOT_EXISTS);
}
return demo;
}
}
package com.study.core.modular.demo.controller;
import com.study.core.modular.common.model.HttpResult;
import com.study.core.modular.demo.model.response.DemoResponse;
import com.study.core.modular.demo.service.DemoService;
import com.study.core.util.JavaBeanUtils;
import io.swagger.annotations.Api;
import io.swagger.annotations.ApiOperation;
import lombok.AllArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.validation.annotation.Validated;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;
import javax.validation.constraints.NotBlank;
@Api(tags = "Demo模块-数据管理Demo接口")
@Validated
@AllArgsConstructor
@Slf4j
@RestController
@RequestMapping("/demoModel/demo")
public class DemoController {
private final DemoService demoService;
@ApiOperation(value = "查询数据详情Demo接口")
@GetMapping("/get")
public HttpResult<DemoResponse> get(@RequestParam @NotBlank String id) {
return HttpResult.success(JavaBeanUtils.map(demoService.get(id), DemoResponse.class));
}
}
结果如下图显示,很完美!
MybatisPlus
的BaseMapper
中提供了很多方法,但有时候还是不能满足所有的业务场景,所以需要在此基础上做扩充。可以在配置类中自定义类继承DefaultSqlInjector
,重写getMethodList
方法,进行自定义方法扩展。package com.study.core.config;
import com.baomidou.mybatisplus.annotation.FieldFill;
import com.baomidou.mybatisplus.core.enums.SqlMethod;
import com.baomidou.mybatisplus.core.injector.AbstractMethod;
import com.baomidou.mybatisplus.core.injector.DefaultSqlInjector;
import com.baomidou.mybatisplus.core.metadata.TableFieldInfo;
import com.baomidou.mybatisplus.core.metadata.TableInfo;
import com.baomidou.mybatisplus.core.toolkit.CollectionUtils;
import com.baomidou.mybatisplus.extension.injector.methods.InsertBatchSomeColumn;
import com.baomidou.mybatisplus.extension.injector.methods.LogicDeleteByIdWithFill;
import com.baomidou.mybatisplus.extension.plugins.MybatisPlusInterceptor;
import com.baomidou.mybatisplus.extension.plugins.inner.OptimisticLockerInnerInterceptor;
import com.baomidou.mybatisplus.extension.plugins.inner.PaginationInnerInterceptor;
import org.apache.ibatis.mapping.MappedStatement;
import org.apache.ibatis.mapping.SqlSource;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import java.util.List;
import static java.util.stream.Collectors.joining;
import static java.util.stream.Collectors.toList;
@Configuration
public class MyBatisPlusConfig {
@Bean
public EasySqlInjector easySqlInjector() {
return new EasySqlInjector();
}
@Bean
public MybatisPlusInterceptor mybatisPlusInterceptor() {
MybatisPlusInterceptor interceptor = new MybatisPlusInterceptor();
//分页查询拦截器
interceptor.addInnerInterceptor(new PaginationInnerInterceptor());
//乐观锁拦截器
interceptor.addInnerInterceptor(new OptimisticLockerInnerInterceptor());
return interceptor;
}
private class EasySqlInjector extends DefaultSqlInjector {
@Override
public List<AbstractMethod> getMethodList(Class<?> mapperClass) {
List<AbstractMethod> methodList = super.getMethodList(mapperClass);
methodList.add(new LogicDeleteByIdWithFill());
methodList.add(new LogicBatchDeleteWithFill());
methodList.add(new InsertBatchSomeColumn());
return methodList;
}
}
private class LogicBatchDeleteWithFill extends AbstractMethod {
/**
* mapper 对应的方法名
*/
private static final String MAPPER_METHOD = "deleteBatchWithFill";
@Override
public MappedStatement injectMappedStatement(Class<?> mapperClass, Class<?> modelClass, TableInfo tableInfo) {
String sql;
SqlMethod sqlMethod = SqlMethod.LOGIC_DELETE;
if (tableInfo.isWithLogicDelete()) {
List<TableFieldInfo> fieldInfos = tableInfo.getFieldList().stream()
.filter(i -> i.getFieldFill() == FieldFill.UPDATE || i.getFieldFill() == FieldFill.INSERT_UPDATE)
.collect(toList());
if (CollectionUtils.isNotEmpty(fieldInfos)) {
String sqlSet = "SET " + fieldInfos.stream().map(i -> i.getSqlSet(ENTITY_DOT)).collect(joining(EMPTY))
+ tableInfo.getLogicDeleteSql(false, false);
sql = String.format(sqlMethod.getSql(), tableInfo.getTableName(), sqlSet,
sqlWhereEntityWrapper(true, tableInfo), "");
} else {
sql = String.format(sqlMethod.getSql(), tableInfo.getTableName(), sqlLogicSet(tableInfo),
sqlWhereEntityWrapper(true, tableInfo), "");
}
SqlSource sqlSource = languageDriver.createSqlSource(configuration, sql, modelClass);
return this.addUpdateMappedStatement(mapperClass, modelClass, MAPPER_METHOD, sqlSource);
} else {
sqlMethod = SqlMethod.DELETE;
sql = String.format(sqlMethod.getSql(), tableInfo.getTableName(), sqlWhereEntityWrapper(true, tableInfo), "");
SqlSource sqlSource = languageDriver.createSqlSource(configuration, sql, modelClass);
return this.addDeleteMappedStatement(mapperClass, MAPPER_METHOD, sqlSource);
}
}
}
}
MybatisPlus配置类中扩展方法,有MP自身的扩展方法还有我们自己添加的自定义扩展方法:
LogicDeleteByIdWithFill
类中的deleteByIdWithFill()
方法:根据ID逻辑删除一条记录,传入参数是一个实体对象,id不能为空LogicBatchDeleteWithFill
类中的deleteBatchWithFill()
方法:根据Wrapper
条件构造器构造条件进行批量删除InsertBatchSomeColumn
类中的insertBatchSomeColumn()
方法:批量插入数据
之后继承MybatisPlus
的BaseMapper
接口,自定义一个扩展方法新接口MyBaseMapper
,之后实体类的mapper
都可以继承此接口,可以在对应的业务层进行批量删除、批量插入等扩展方法操作。新接口如下:
package com.study.core.modular.common.dao;
import com.baomidou.mybatisplus.core.conditions.Wrapper;
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import com.baomidou.mybatisplus.core.toolkit.Constants;
import org.apache.ibatis.annotations.Param;
import java.util.Collection;
public interface MyBaseMapper<T> extends BaseMapper<T> {
int deleteByIdWithFill(T entity);
int deleteBatchWithFill(@Param(Constants.ENTITY) T entity, @Param(Constants.WRAPPER) Wrapper<T> wrapper);
Integer insertBatchSomeColumn(Collection<T> entityList);
}
在application.properties
配置文件中配置全局逻辑删除标志位,此配置项可取代实体类中的@TableLogic
注解,配置一次则全部实体类中只要存在is_del
字段,调用相应的查询、删除方法时都会加上对应的sql
语句。(where is_del = '0' 或 set is_del = '1')
# mybatisplus
mybatis-plus.global-config.db-config.logic-delete-field=is_del
mybatis-plus.global-config.db-config.logic-not-delete-value=0
mybatis-plus.global-config.db-config.logic-delete-value=1
所有实体类数据,都存在几个相同的字段:id
、创建人id
、创建时间
、更新人id
、更新时间
以及逻辑删除标志位
,有些还会有组织机构id等等。为了不想每次在读写数据库的时候,都去重复的更新相应的字段,我们可以做全局统一处理。
首先我们要统一数据库表结构内的基础字段:
id
:唯一标识create_id
:创建人idcreate_time
:创建时间update_id
:更新人idupdate_time
:更新时间is_del
:逻辑删除 0-否 1-是然后把这几个字段抽出来定义成一个基类,所有的实体类都会继承基类:
id
后续可以采用雪花算法或者号段模式去自动生成,他一定是新增数据时自动生成的默认值,现在暂时先不谈。
create_id
与create_time
只有在新增数据时才会自动填充,所以使用INSERT
update_id
与update_time
新增数据与更新数据时都要读写数据库,所以使用INSERT_UPDATE
is_del
只有在删除时自动更新为1
,新增数据时默认为0
,所以只需要INSERT
package com.study.core.modular.common.model.entity;
import com.baomidou.mybatisplus.annotation.*;
import com.fasterxml.jackson.annotation.JsonFormat;
import io.swagger.annotations.ApiModelProperty;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
import lombok.experimental.SuperBuilder;
import java.util.Date;
@Data
@AllArgsConstructor
@NoArgsConstructor
@SuperBuilder
public class BaseDO {
@ApiModelProperty(value = "id")
@TableId(value = "id", type = IdType.ASSIGN_UUID)
private String id;
public static final String ID = "id";
@ApiModelProperty(value = "创建人id")
@TableField(value = "create_id", fill = FieldFill.INSERT)
private String createId;
public static final String CREATE_ID = "createId";
@ApiModelProperty(value = "创建时间")
@TableField(value = "create_time", fill = FieldFill.INSERT)
@JsonFormat(timezone = "GMT+8", pattern = "yyyy-MM-dd HH:mm:ss")
private Date createTime;
public static final String CREATE_TIME = "createTime";
@ApiModelProperty(value = "更新人id")
@TableField(value = "update_id", fill = FieldFill.INSERT_UPDATE)
private String updateId;
public static final String UPDATE_ID = "updateId";
@ApiModelProperty(value = "更新时间")
@TableField(value = "update_time", fill = FieldFill.INSERT_UPDATE)
@JsonFormat(timezone = "GMT+8", pattern = "yyyy-MM-dd HH:mm:ss")
private Date updateTime;
public static final String UPDATE_TIME = "updateTime";
@ApiModelProperty(value = "逻辑删除 0-否 1-是")
@TableField(value = "is_del", fill = FieldFill.INSERT)
@TableLogic
private Integer isDel;
public static final String IS_DEL = "isDel";
}
创建基类后,修改Demo
实体类,继承基类:
package com.study.core.modular.demo.model.entity;
import com.baomidou.mybatisplus.annotation.TableField;
import com.baomidou.mybatisplus.annotation.TableName;
import com.study.core.modular.common.model.entity.BaseDO;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.EqualsAndHashCode;
import lombok.NoArgsConstructor;
import lombok.experimental.Accessors;
import lombok.experimental.SuperBuilder;
@Data
@EqualsAndHashCode(callSuper = true)
@Accessors(chain = true)
@SuperBuilder
@AllArgsConstructor
@NoArgsConstructor
@TableName(value = "tbl_demo")
public class Demo extends BaseDO {
/**
* 姓名
*/
@TableField(value = "`name`")
@ApiModelProperty(value = "姓名")
private String name;
/**
* 手机号
*/
@TableField(value = "phone")
@ApiModelProperty(value = "手机号")
private String phone;
}
自定义一个处理类,实现元数据对象处理接口MetaObjectHandler
。为新增数据与更新数据操作时,进行自动填充基本字段信息操作。
重写MetaObjectHandler
的两个方法:
insertFill
:新增数据自动填充逻辑处理updateFill
:更新数据自动填充逻辑处理具体实现如下:
package com.study.core.handler;
import cn.hutool.core.date.DateUtil;
import com.baomidou.mybatisplus.core.handlers.MetaObjectHandler;
import com.study.core.modular.common.enums.YesOrNoEnum;
import com.study.core.modular.common.model.entity.BaseDO;
import org.apache.ibatis.reflection.MetaObject;
import org.springframework.stereotype.Component;
import java.util.Date;
@Component
public class SetBasicInfoHandler implements MetaObjectHandler {
@Override
public void insertFill(MetaObject metaObject) {
Object o = metaObject.getOriginalObject();
if (o instanceof BaseDO) {
BaseDO baseDO = (BaseDO) o;
//TODO 通过上下文获取当前登录用户信息后再填充创建人id或更新人id
if (baseDO.getCreateTime() == null) {
baseDO.setCreateTime(DateUtil.date());
}
if (baseDO.getUpdateTime() == null) {
baseDO.setUpdateTime(DateUtil.date());
}
baseDO.setIsDel(YesOrNoEnum.N.getCode());
} else {
strictInsertFill(metaObject, BaseDO.CREATE_TIME, Date.class, DateUtil.date());
strictInsertFill(metaObject, BaseDO.UPDATE_TIME, Date.class, DateUtil.date());
}
}
@Override
public void updateFill(MetaObject metaObject) {
Object o = metaObject.getOriginalObject();
if (o instanceof BaseDO) {
BaseDO baseDO = (BaseDO) o;
//TODO 通过上下文获取当前登录用户信息后再填充更新人id
baseDO.setUpdateTime(DateUtil.date());
} else {
strictUpdateFill(metaObject, BaseDO.UPDATE_TIME, Date.class, DateUtil.date());
}
}
}
(1)新增数据测试,观察是否自动填充create_time、update_time、is_del这三个字段
先定义请求实体类,新增数据和编辑数据两个接口的请求实体类可以是同一个,不同的校验规则可以通过校验组进行校验区分。在model
包下新建request
包,其中创建请求实体类DemoRequest
。
package com.study.core.modular.demo.model.request;
import io.swagger.annotations.ApiModelProperty;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Data;
import lombok.NoArgsConstructor;
import javax.validation.constraints.NotBlank;
@Data
@AllArgsConstructor
@NoArgsConstructor
@Builder
public class DemoRequest {
@ApiModelProperty(value = "id")
@NotBlank(message = "id不能为空", groups = { DemoRequest.Edit.class })
private String id;
@ApiModelProperty(value = "名称")
@NotBlank(message = "名称不能为空")
private String name;
@ApiModelProperty(value = "手机号")
@NotBlank(message = "手机号不能为空")
private String phone;
/**
* 修改校验组
*/
public interface Edit {
}
}
编写核心业务逻辑
package com.study.core.modular.demo.service;
import com.study.core.modular.common.model.entity.BaseDO;
import com.study.core.modular.demo.dao.DemoMapper;
import com.study.core.modular.demo.model.entity.Demo;
import com.study.core.modular.demo.model.request.DemoRequest;
import com.study.core.util.JavaBeanUtils;
import lombok.AllArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
@Service
@AllArgsConstructor
@Slf4j
public class DemoService {
private final DemoMapper demoMapper;
@Transactional(rollbackFor = Exception.class)
public void add(DemoRequest request) {
Demo demo = JavaBeanUtils.map(request, Demo.class, BaseDO.ID);
demoMapper.insert(demo);
}
}
创建新接口:新增数据接口
package com.study.core.modular.demo.controller;
import com.study.core.modular.common.model.HttpResult;
import com.study.core.modular.demo.model.request.DemoRequest;
import com.study.core.modular.demo.service.DemoService;
import io.swagger.annotations.Api;
import io.swagger.annotations.ApiOperation;
import lombok.AllArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.validation.annotation.Validated;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
@Api(tags = "Demo模块-数据管理Demo接口")
@Validated
@AllArgsConstructor
@Slf4j
@RestController
@RequestMapping("/demoModel/demo")
public class DemoController {
private final DemoService demoService;
@ApiOperation(value = "新增数据Demo接口")
@PostMapping("/add")
public HttpResult<Void> add(@RequestBody @Validated DemoRequest request) {
demoService.add(request);
return HttpResult.success();
}
}
在swagger
上测试接口,输入测试实例,发送请求,返回数据库实际结果:可以看到创建时间和更新时间都是自动记录的新增数据时的当前时间,逻辑删除标志位默认为否(0)。
(2)编辑数据测试,观察update_time字段是否自动更新
同样使用新增数据时的请求实体类,开始编写核心业务逻辑
package com.study.core.modular.demo.service;
import cn.hutool.core.util.ObjectUtil;
import com.study.core.exception.BizException;
import com.study.core.modular.common.enums.HttpResultCode;
import com.study.core.modular.demo.dao.DemoMapper;
import com.study.core.modular.demo.model.entity.Demo;
import com.study.core.modular.demo.model.request.DemoRequest;
import com.study.core.util.JavaBeanUtils;
import lombok.AllArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
@Service
@AllArgsConstructor
@Slf4j
public class DemoService {
private final DemoMapper demoMapper;
@Transactional(rollbackFor = Exception.class)
public void edit(DemoRequest request) {
Demo demo = demoMapper.selectById(request.getId());
if (ObjectUtil.isNull(demo)) {
throw new BizException(HttpResultCode.DATA_NOT_EXISTS);
}
JavaBeanUtils.map(request, demo);
demoMapper.updateById(demo);
}
}
创建新接口:编辑数据接口
package com.study.core.modular.demo.controller;
import com.study.core.modular.common.model.HttpResult;
import com.study.core.modular.demo.model.request.DemoRequest;
import com.study.core.modular.demo.service.DemoService;
import io.swagger.annotations.Api;
import io.swagger.annotations.ApiOperation;
import lombok.AllArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.validation.annotation.Validated;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
@Api(tags = "Demo模块-数据管理Demo接口")
@Validated
@AllArgsConstructor
@Slf4j
@RestController
@RequestMapping("/demoModel/demo")
public class DemoController {
private final DemoService demoService;
@ApiOperation(value = "编辑数据Demo接口")
@PostMapping("/edit")
public HttpResult<Void> edit(@RequestBody @Validated(DemoRequest.Edit.class) DemoRequest request) {
demoService.edit(request);
return HttpResult.success();
}
}
在swagger
上测试接口,输入测试实例,发送请求,返回数据库实际结果:可以看到更新时间已被更改。
(3)删除数据测试,观察is_del字段是否自动更新成1
首先定义一个删除数据接口的请求实体类(在这里提一下,一般写接口都是POST
,读接口都是GET
,POST接口中的请求参数都是放在请求体中的,一般需要对象进行包装,更方便)
package com.study.core.modular.demo.model.request;
import io.swagger.annotations.ApiModelProperty;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Data;
import lombok.NoArgsConstructor;
import javax.validation.constraints.NotBlank;
@Data
@AllArgsConstructor
@NoArgsConstructor
@Builder
public class DemoDeleteRequest {
@ApiModelProperty(value = "id")
@NotBlank(message = "id不能为空")
private String id;
}
编写核心业务
package com.study.core.modular.demo.service;
import cn.hutool.core.util.ObjectUtil;
import com.study.core.exception.BizException;
import com.study.core.modular.common.enums.HttpResultCode;
import com.study.core.modular.demo.dao.DemoMapper;
import com.study.core.modular.demo.model.entity.Demo;
import com.study.core.modular.demo.model.request.DemoDeleteRequest;
import lombok.AllArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
@Service
@AllArgsConstructor
@Slf4j
public class DemoService {
private final DemoMapper demoMapper;
@Transactional(rollbackFor = Exception.class)
public void delete(DemoDeleteRequest request) {
Demo demo = demoMapper.selectById(request.getId());
if (ObjectUtil.isNull(demo)) {
throw new BizException(HttpResultCode.DATA_NOT_EXISTS);
}
demoMapper.deleteByIdWithFill(demo);
}
}
创建新接口:根据id删除数据
package com.study.core.modular.demo.controller;
import com.study.core.modular.common.model.HttpResult;
import com.study.core.modular.demo.model.request.DemoDeleteRequest;
import com.study.core.modular.demo.service.DemoService;
import io.swagger.annotations.Api;
import lombok.AllArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.validation.annotation.Validated;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
@Api(tags = "Demo模块-数据管理Demo接口")
@Validated
@AllArgsConstructor
@Slf4j
@RestController
@RequestMapping("/demoModel/demo")
public class DemoController {
private final DemoService demoService;
@ApiOperation(value = "删除数据Demo接口")
@PostMapping("/delete")
public HttpResult<Void> delete(@RequestBody @Validated DemoDeleteRequest request) {
demoService.delete(request);
return HttpResult.success();
}
}
在swagger
上测试接口,输入测试实例,发送请求,返回数据库实际结果:可以看到逻辑删除标志位已被更改为1,同时更新时间字段也被修改更新了。
(4)修改批量删除数据接口,根据id删除单条记录改为批量删除指定记录,使用自定义扩展方法
deleteBatchWithFill()
修改控制层接口参数
package com.study.core.modular.demo.controller;
import com.study.core.modular.common.model.HttpResult;
import com.study.core.modular.demo.service.DemoService;
import io.swagger.annotations.Api;
import io.swagger.annotations.ApiOperation;
import lombok.AllArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.validation.annotation.Validated;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import javax.validation.constraints.NotEmpty;
import java.util.List;
@Api(tags = "Demo模块-数据管理Demo接口")
@Validated
@AllArgsConstructor
@Slf4j
@RestController
@RequestMapping("/demoModel/demo")
public class DemoController {
private final DemoService demoService;
@ApiOperation(value = "批量删除数据Demo接口")
@PostMapping("/delete")
public HttpResult<Void> delete(@RequestBody @NotEmpty List<String> idList) {
demoService.delete(idList);
return HttpResult.success();
}
}
核心业务实现
package com.study.core.modular.demo.service;
import com.baomidou.mybatisplus.core.toolkit.Wrappers;
import com.study.core.modular.demo.dao.DemoMapper;
import com.study.core.modular.demo.model.entity.Demo;
import lombok.AllArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import java.util.List;
@Service
@AllArgsConstructor
@Slf4j
public class DemoService {
private final DemoMapper demoMapper;
@Transactional(rollbackFor = Exception.class)
public void delete(List<String> idList) {
demoMapper.deleteBatchWithFill(Demo.builder()
.build(), Wrappers.lambdaQuery(Demo.class)
.in(Demo::getId, idList));
}
}
开始测试,先新增几条数据,然后将这些数据的id全部传入测试用例中进行批量删除。单条数据删除也可以使用此接口,此时请求参数的id数组内元素只有一个。