增删改事务 - 01 - 增删改流程
1. 流程概述
增删改作为事务相关的处理,需要格外注意入参的正确性,不正确的参数可能会导致异常
增删改操作时,用 DTO 来封装待修改数据,对DTO进行入参校验,校验完毕后,传入 service 层,将 DTO 转为 entity,存储到数据库
四个词就能概括下图:入参,校验,转换,入库
2. 流程细化
在这里举个简单的流程栗子:
2.1 入参 + 校验
2.1.1 构建DTO入参类,并做基础校验
/** DTO,非空校验添加的地方 */
@Data
@AllArgsConstructor
@NoArgsConstructor
public class WorkOperatorDTO {
/** 主键ID */
@TableId(value = "id", type = IdType.AUTO)
private Long id;
/** 作业单位ID */
@NotNull(message = "作业单位ID不能为空")
private Long workCompId;
/** 电子工牌 - 非必填,可以设为 Null 或者 空字符串,修改 MP 的更新策略,使其不会跳过 NULL 的列 */
@TableField(updateStrategy = FieldStrategy.IGNORED)
private String eleWorkCard;
/** 身份证号 */
@NotBlank(message = "身份证号不能为空")
@Size(max = 18, message = "身份证号长度不能超过18位")
private String idCardNumber;
/** 关联的 ids */
@NotEmpty(message = "关联的 ids 不能为空")
List<Long> ids;
/** 入职日期 */
@NotBlank(message = "入职日期不能为空,格式yyyy-MM-dd")
@Pattern(regexp = "^\\d{4}-\\d{2}-\\d{2}$", message = "入职日期格式错误(yyyy-MM-dd)")
private String comeInDate;
}
2.1.2 在 controller 里面做复杂校验
注: ValidationUtils 是自己写的一个小工具类,主要判断字符串是否匹配正则表达式
/** Controller层 */
import javax.annotation.Resource;
@RestController
@RequestMapping("/work-operator")
public class WorkOperatorController {
@Resource
private IWorkOperatorService workOperatorService;
/**
* 入参校验,能够统一处理,则放到统一校验类里面,否则放到自己的类里处理
* @param dto 增改时的入参类
* @return
*/
private R checkDTO(WorkOperatorDTO dto) {
// 简单校验:身份证号、身份证号和性别匹配
if (dto.getIdCardNumber().length() != ValidationUtils.CHINA_ID_MAX_LENGTH) {
return R.fail(ConstantMessage.NOT_CHINA_ID_MAX_LENGTH);
}
if (!ValidationUtils.matchTargetRegex(dto.getIdCardNumber(), ValidationUtils.ID_CARD_REGEX)) {
return R.fail(ConstantMessage.NOT_CHINA_ID);
}
// 校验性别
int sexRes = Integer.parseInt(dto.getIdCardNumber().substring(16, 17)) % 2 == 0 ? 2 : 1;
if (sexRes != dto.getSex()) {
return R.fail(ConstantMessage.SEX_NOT_MATCH_CHINA_ID);
}
// 校验手机号
if (!ValidationUtils.matchTargetRegex(dto.getPhone(), ValidationUtils.PHONE_REGEX)) {
return R.fail(ConstantMessage.PHONE_NOT_MATCH_REGEX);
}
// 校验电子工牌,不为空时,需要判断是否匹配正则表达式
if (StringUtils.isNotEmpty(dto.getEleWorkCard())) {
if (!ValidationUtils.matchTargetRegex(dto.getEleWorkCard(), ValidationUtils.NUM_ENG_LIMIT_30)) {
return R.fail(ConstantMessage.PLEASE_TRY_AGAIN + ConstantMessage.LENGTH_FORMAT_ERR);
} else {
// 判断电子工牌是否存在
boolean exist = workOperatorService.lambdaQuery().eq(WorkOperator::getEleWorkCard, dto.getEleWorkCard()).ne(dto.getId() != null, WorkOperator::getEleWorkCard, dto.getEleWorkCard()).exists();
if (exist) {
return R.fail(ConstantMessage.PRE_FIX + ConstantMessage.ELE_CARD_EXIST + ConstantMessage.PLEASE_TRY_AGAIN);
}
}
}
// 校验,中文30字符以内
if (!ValidationUtils.matchTargetRegex(dto.getName(), ValidationUtils.ZH_REGEX_LIMIT_30)) {
return R.fail(ConstantMessage.PRE_FIX + ConstantMessage.NAME_ERROR + ConstantMessage.PLEASE_TRY_AGAIN);
}
// 手机号已存在时,
if (workOperatorService.lambdaQuery().eq(WorkOperator::getPhone, dto.getPhone()).ne(dto.getId() != null, WorkOperator::getId, dto.getId()).exists()) {
return R.fail(ConstantMessage.PRE_FIX + ConstantMessage.PHONE_EXIST + ConstantMessage.PLEASE_TRY_AGAIN);
}
// 同理,可校验其他的 entity 属性
return null;
}
}
对手机号和电子工牌唯一性校验的解释:
ne(dto.getId() != null, WorkOperator::getId, dto.getId()) 这段用来查询与当前 id 不同的数据
构建DTO有两种情况,一是新增,二是修改
新增时,没有 id 字段,ne(dto.getId() != null, WorkOperator::getId, dto.getId()) 这段不会执行
修改时,因为id存在,ne(dto.getId() != null, WorkOperator::getId, dto.getId()) 这段查询代码会执行,用来判断当前值除去自身外是否唯一,避免了修改时,与原值相同而无法入库的情况
这段代码可以使用MP的写法封装一下,后续封装好后,过来更新
2.3 在Controller里让两个校验生效
import javax.validation.Valid;
@RestController
@RequestMapping("/work-operator")
public class Controller {
@Resource
private IWorkOperatorService workOperatorService;
/**
* 入参校验,能够统一处理,则放到统一校验类里面,否则放到自己的类里处理
* @param dto 增改时的入参类
* @return
*/
private R checkDTO(WorkOperatorDTO dto) {
// 上面的入参复杂校验
}
/** 新增 */
@PostMapping("/create")
public R<Object> create(@Valid @RequestBody WorkOperatorDTO saveDTO, @User UserBO userBO) {
// 判断入参是否正确
if (checkDTO(saveDTO) != null) {
return checkDTO(saveDTO);
}
// 入参正确后进入 service 层
return workOperatorService.createWorkOperator(saveDTO) ?
R.ok(ConstantMessage.CREATE_SUCCESS) : R.fail(ConstantMessage.CREATE_FAIL);
}
/** 修改 */
@PostMapping("/update")
public R<Object> update(@Valid @RequestBody WorkOperatorDTO updateDTO, @User UserBO userBO) {
// 判断入参是否正确
if (checkDTO(updateDTO) != null) {
return checkDTO(updateDTO);
}
// 入参正确后进入 service 层
return workOperatorService.updateWorkOperator(updateDTO) ?
R.ok(ConstantMessage.UPDATE_SUCCESS) : R.fail(ConstantMessage.UPDATE_FAIL);
}
}
注1:@Valid 必须添加后,才能达到第一步简单校验的效果
注2:第二步的复杂校验可直接放在 controller 里处理
2.2 转换
往期文章里写了转换类的方法,使用同样的方法,将 DTO -> entity
详情跳转到:下面文章中,1.2 类的调整方法
给个实体类 - entity 的栗子:
/** entity */
@Data
@NoArgsConstructor
@AllArgsConstructor
@EqualsAndHashCode(callSuper = false)
@Accessors(chain = true)
@TableName("work_operator")
public class WorkOperator implements Serializable {
private static final long serialVersionUID = 1L;
/** 主键ID */
@TableId(value = "id", type = IdType.AUTO)
private Long id;
/** 作业单位ID */
private Long workCompId;
/** 电子工牌 - 非必填,可以设为 Null 或者 空字符串,修改 MP 的更新策略,使其不会跳过 NULL 的列 */
@TableField(updateStrategy = FieldStrategy.IGNORED)
private String eleWorkCard;
/** 身份证号 */
private String idCardNumber;
/** 关联的 ids */
@TableField(typeHandler = JacksonTypeHandler.class)
List<Long> ids;
}
Service 层的栗子:
public class Service {
@Resource
private WorkOperatorDAO workOperatorDAO;
/** 新增方法 */
@Override
@Transactional(rollbackFor = Exception.class)
public boolean createWorkOperator(WorkOperatorDTO saveDTO) {
// DTO -> entity
// 需要额外查询 片区、片区id、作业单位名称、年龄
WorkOperator saveOne = transDTO2Entity(saveDTO);
saveOne.setId(null);
// 保存
return workOperatorDAO.insertWorkOperator(saveOne);
}
/** 更新方法 */
@Override
@Transactional(rollbackFor = Exception.class)
public boolean updateWorkOperator(WorkOperatorDTO updateDTO) {
// DTO -> entity
WorkOperator updateOne = transDTO2Entity(updateDTO);
workOperatorDAO.updateWorkOperator(updateOne);
return true;
}
/** DTO -> 实体类 */
private WorkOperator transDTO2Entity(WorkOperatorDTO saveDTO) {
WorkOperator one = new WorkOperator(saveDTO);
one.setAge(IdCardUtil.getAgeByIdNumber(saveDTO.getIdCardNumber()));
return one;
}
}
2.3 入库
- 入库操作放在了 DAO 层,举个栗子
import org.apache.commons.lang3.StringUtils;
/** DAO层 */
@Repository
public class DAO {
/** 更新 */
public void updateWorkOperator(WorkOperator workOperator) {
/** 如果前端传的是空字符串,会影响mysql 的 uniqueKey,故处理为 null */
if (StringUtils.isEmpty(workOperator.getEleWorkCard())) {
workOperator.setEleWorkCard(null);
}
try {
workOperatorMapper.updateById(workOperator);
} catch (Exception e) {
checkException(e);
}
}
/** 新增 */
public boolean insertWorkOperator(WorkOperator workOperator) {
try {
return workOperatorMapper.insert(workOperator) > 0;
} catch (Exception e) {
checkException(e);
}
return true;
}
/** 处理异常,部分通用异常可放到统一异常处理器里面,特殊异常可放到自己的类里处理 */
private void checkException(Exception e) {
if (StringUtils.containsIgnoreCase(e.getMessage(), "duplicate entry") && StringUtils.containsIgnoreCase(e.getMessage(), "work_operator.un_id_card_number")) {
throw new IcpBizException("该身份证号已有职务,请重新确认!");
}
if (StringUtils.containsIgnoreCase(e.getMessage(), "duplicate entry") && StringUtils.containsIgnoreCase(e.getMessage(), "work_operator.un_ele_card")) {
throw new IcpBizException("该电子工牌已被使用,请重新确认!");
}
if (StringUtils.containsIgnoreCase(e.getMessage(), "duplicate entry") && StringUtils.containsIgnoreCase(e.getMessage(), "work_operator.un_phone")) {
throw new IcpBizException("该手机号已被使用,请重新确认!");
}
}
}
- 注1:IcpBizException 是个自定义的类,设置了全局异常捕获,后续会讲到
- 注2:异常里的三个字段均设置了 mysql 的 uniqueKey
- 注3:记得使用 @Repository 注解,暴露 class 给 spring 容器
- 注4:StringUtils.containsIgnoreCase(str A, str B) 这段代码用于判断字符串A中是否包含字符串B,同时忽略大小写。