自定义校验-数据库字段唯一性校验

场景:经常需要校验某张表的某个字段是否唯一。比如王者荣耀取名老是提示 名称已存在,这就是一种唯一性校验。

条件:

springboot已经提供了很多的校验,比如@NotNull、@NotEmpty 等。这些其实是JSR303规范。

那么,如果自定义注解实现我的需求呢?

本博文内容依赖 mybatis-plus、反射工具类。

一、已有资源

1、表 act_cx_category

CREATE TABLE `act_cx_category` (
  `category_id` varchar(255) CHARACTER SET utf8mb3 COLLATE utf8_general_ci NOT NULL COMMENT '分类id',
  `category_name` varchar(32) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci DEFAULT NULL COMMENT '分类名称',
  `parent_id` varchar(64) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci DEFAULT NULL COMMENT '父id',
  `icon` varchar(255) CHARACTER SET utf8mb3 COLLATE utf8_general_ci DEFAULT NULL COMMENT '图标',
  `sort` double DEFAULT NULL COMMENT '排序',
  `app_id` varchar(255) DEFAULT NULL COMMENT '应用id',
  PRIMARY KEY (`category_id`) USING BTREE
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb3 ROW_FORMAT=DYNAMIC COMMENT='分类表';

2、对应的实体类


import com.baomidou.mybatisplus.annotation.IdType;
import com.baomidou.mybatisplus.annotation.TableField;
import com.baomidou.mybatisplus.annotation.TableId;
import com.baomidou.mybatisplus.annotation.TableName;
import java.io.Serializable;
import lombok.Data;

/**
 * 分类表
 * @TableName act_cx_category
 */
@TableName(value ="act_cx_category")
@Data
public class ActCxCategoryEntity implements Serializable {

    /**
     * 分类id
     */
    @TableId(type= IdType.ASSIGN_ID)
    private String categoryId;

    /**
     * 分类名称
     */
    private String categoryName;

    /**
     * 父id
     */
    private String parentId;

    /**
     * 图标
     */
    private String icon;

    /**
     * 排序
     */
    private Double sort;

    /**
     * 应用id
     */
    private String appId;

    @TableField(exist = false)
    private static final long serialVersionUID = 1L;
}

3、对应的mapper


import com.jxctjt.workflow.domain.entity.ActCxCategoryEntity;
import com.baomidou.mybatisplus.core.mapper.BaseMapper;

/**
* @description 针对表【act_cx_category(分类表)】的数据库操作Mapper
* @Entity com.jxctjt.workflow.domain.entity.ActCxCategoryEntity
*/
public interface ActCxCategoryMapper extends BaseMapper<ActCxCategoryEntity> {

}

4、controller层

  @PostMapping("/test")
  public Result test(@Validated(AddGroup.class) @RequestBody CategoryBo bo){
    log.info(bo.toString());
    //  逻辑
    return Result.ok("测试成功!");
  }

5、需要校验的Bo对象


import com.jxctjt.workflow.common.validate.group.AddGroup;
import com.jxctjt.workflow.common.validate.group.EditGroup;
import com.jxctjt.workflow.common.validate.group.anno.UniqueExtendAppIdImpl;
import com.jxctjt.workflow.common.validate.group.anno.UniqueField;
import com.jxctjt.workflow.common.validate.group.anno.UniqueType;
import com.jxctjt.workflow.domain.entity.ActCxCategoryEntity;
import com.jxctjt.workflow.mapper.ActCxCategoryMapper;
import io.swagger.annotations.ApiModel;
import io.swagger.annotations.ApiModelProperty;
import java.io.Serializable;
import javax.validation.constraints.NotNull;
import lombok.Data;

/**
 * @author xgz
 */
@Data
@ApiModel("分类参数对象")
public class CategoryBo implements Serializable {

  private static final long serialVersionUID = 1L;

  @ApiModelProperty("分类id")
  @NotNull(message = "分类id不能为空", groups = {EditGroup.class})
  private String categoryId;

  /**
   * 分类名称
   */
  @ApiModelProperty("分类名称")
  @NotNull(message = "分类名称不能为空", groups = {AddGroup.class, EditGroup.class})
  private String categoryName;

  /**
   * 图标
   */
  @ApiModelProperty("图标")
  private String icon;

  /**
   * 排序
   */
  @ApiModelProperty("排序")
  @NotNull(message = "排序不能为空", groups = {AddGroup.class, EditGroup.class})
  private Double sort;
}

二、实现

1、UniqueType 注解 

通过 @Constraint(validatedBy = {UniqueValidator.class}) 指定这个注解使用 UniqueValidator进行校验。

这个注解只能标注在类上,当@Validated注解标注在 请求实体上时,就会进入到

UniqueValidator 里面。


import static java.lang.annotation.ElementType.TYPE;
import static java.lang.annotation.RetentionPolicy.RUNTIME;
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import java.lang.annotation.Documented;
import java.lang.annotation.Retention;
import java.lang.annotation.Target;
import javax.validation.Constraint;
import javax.validation.Payload;

/**
 * 校验参数唯一
 * 需要和 UniqueField 注解 搭配使用
 * 场景:要求 用户名唯一,但是数据库用户名没有设置唯一键。
 * @author xgz
 */
@Documented
@Constraint(validatedBy = {UniqueValidator.class})
@Target({TYPE})
@Retention(RUNTIME)
public @interface UniqueType {
  Class<?>  entity();
  // 访问数据库的服务
  Class<? extends BaseMapper>  mapper();
  /**
   * 是否开启校验
   *
   * @return 是否强制校验的boolean值
   */
  boolean required() default true;
  String message() default "{数据库已存在}";

  Class<?>[] groups() default {};

  Class<? extends Payload>[] payload() default {};
  /** 不得更改默认值 **/
  Class<? extends UniqueExtend> extend() default UniqueExtend.class;
}

2、UniqueField 注解

这个注解只能标注在字段上。

UniqueType 注解必须同时搭配UniqueField注解,才会生效。原因是UniqueValidator的isValid

方法 的实现逻辑决定的。

import static java.lang.annotation.ElementType.FIELD;
import static java.lang.annotation.RetentionPolicy.RUNTIME;

import java.lang.annotation.Documented;
import java.lang.annotation.Retention;
import java.lang.annotation.Target;

/**
 * 数据库唯一校验
 * 场景:比如要求 用户名唯一,但是数据库用户名没有设置唯一键。
 * @author xgz
 */
@Documented
@Target({FIELD})
@Retention(RUNTIME)
public @interface UniqueField {

  /**
   * 是否开启校验
   *
   * @return 是否强制校验的boolean值
   */
  boolean required() default true;
  String message();

  /**
   * 拓展类  比如 SQL还需要拼接上 appId 字段进行查询
   * 不得更改extend的默认值
   * **/
  Class<? extends UniqueExtend> extend() default UniqueExtend.class;
}

3、UniqueValidator 校验类

当标注了 @UniqueType 注解的参数类,执行校验时,会首先 执行 initialize方法,这里是初始化的意思,可以获取到 @UniqueType 的参数。

然后会执行 isValid 方法。isValid方法返回true表示校验通过,放行。返回false则表示失败,会抛出异常,默认message是@UniqueType注解的参数message。


import cn.hutool.core.text.CharSequenceUtil;
import cn.hutool.core.util.ReflectUtil;
import cn.hutool.extra.spring.SpringUtil;
import com.baomidou.mybatisplus.annotation.TableId;
import com.baomidou.mybatisplus.core.conditions.AbstractWrapper;
import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import com.jxctjt.workflow.common.constant.SepConstant;
import com.jxctjt.workflow.common.exception.SysException;
import com.jxctjt.workflow.common.util.ReflectUtils;
import java.lang.reflect.Field;
import java.util.Objects;
import javax.validation.ConstraintValidator;
import javax.validation.ConstraintValidatorContext;
import lombok.extern.slf4j.Slf4j;
import org.jetbrains.annotations.Nullable;

/**
 * @author xgz
 */
@Slf4j
public class UniqueValidator implements ConstraintValidator<UniqueType, Object> {

  /**
   * 是否强制校验
   */
  private boolean required;

  /**
   * 操作数据库的服务类
   **/
  private Class<? extends BaseMapper> mapper;

  private Class entity;
  private UniqueExtend extend;
  /**
   * 实体类中 主键名称
   * <p>
   */
  private String primaryKeyName;

  /**
   * 实体类中 主键值
   */
  private Object primaryKeyValue;

  @Override
  public void initialize(UniqueType anno) {
    this.required = anno.required();
    this.mapper = anno.mapper();
    if (null == mapper) {
      throw new SysException("service不能为空");
    }
    this.entity = anno.entity();
    Class<? extends UniqueExtend> defaultVal = anno.extend();
    this.extend = getUniqueExtend(defaultVal);
  }

  /**
   * 返回true 表示验证成功 返回false 表示验证失败,会抛出异常
   **/
  @Override
  public boolean isValid(Object obj, ConstraintValidatorContext context) {
    if (required) {
      BaseMapper bean = SpringUtil.getBean(mapper);
      if (null == bean) {
        throw new SysException("Unique 注解的 mapper 必须是Bean");
      }
      return doValid(bean, obj, context);
    }
    return true;
  }

  private boolean doValid(BaseMapper mapper, Object obj, ConstraintValidatorContext context) {
    // 尝试获取表的主键名 和传递过来的主键值。通过是否有主键值判断是新增还是更新。
    getPrimaryNameAndValue(obj);
  
    Field[] fields = ReflectUtils.getFields(obj.getClass());
    for (Field field : fields) {
      if (field.isAnnotationPresent(UniqueField.class)) {
        UniqueField anno = field.getAnnotation(UniqueField.class);
        String message = anno.message();
        String name = field.getName();
        Class<? extends UniqueExtend> filedExtend = anno.extend();
        UniqueExtend uniqueExtendObj = getUniqueExtend( filedExtend);
        String dbField = toDbField(name);
        Object value = ReflectUtils.invokeGetter(obj, name);
        if (null == value){
          continue;
        }
        Object entity = ReflectUtils.newInstance(this.entity);
        AbstractWrapper wrapper = new QueryWrapper(entity).eq(true, dbField, value)
            .ne(Objects.nonNull(primaryKeyValue), toDbField(primaryKeyName), primaryKeyValue);
        // 执行拓展
        try {
          if (uniqueExtendObj == null){
            if (this.extend != null){
              wrapper = this.extend.apply(wrapper, entity, obj, field);
            }
          }else{
            wrapper = uniqueExtendObj.apply(wrapper, entity, obj, field);
          }
        }catch (Exception e){
          log.error("执行拓展出错!");
          wrapper = new QueryWrapper(entity).eq(true, dbField, value)
              .ne(Objects.nonNull(primaryKeyValue), toDbField(primaryKeyName), primaryKeyValue);
        }
        int i = mapper.selectCount(wrapper).intValue();
        if (i > 0){
          log.error(message);
          //禁用默认message值,不禁用会在原有默认的message的基础上进行拼接
          context.disableDefaultConstraintViolation();
          context.buildConstraintViolationWithTemplate(message).addConstraintViolation();
          return false;
        }
      }
    }
    return true;
  }

  /**
   * 获取 拓展类对象
   * 优先在 容器中获取
   *  **/
  @Nullable
  private static UniqueExtend 
                getUniqueExtend(Class<? extends UniqueExtend> extendClass){

    UniqueExtend uniqueExtendObj = null;
   if (!UniqueExtend.class.equals(extendClass)) {
          uniqueExtendObj = SpringUtil.getBean(extendClass);
          if (null == uniqueExtendObj){
            uniqueExtendObj = ReflectUtil.newInstance(extendClass);
          }
    }
    return uniqueExtendObj;
  }

  private void getPrimaryNameAndValue(Object obj) {
    Field[] fields = ReflectUtils.getFields(entity);
    for (Field field : fields) {
      if (field.isAnnotationPresent(TableId.class)) {
        this.primaryKeyName = field.getName();
        primaryKeyValue = ReflectUtils.invokeGetter(obj, this.primaryKeyName);
        break;
      }
    }
  }

  private String toDbField(String name) {
    if (CharSequenceUtil.isBlank(name)) {
      throw new SysException("要验证的字段名不能为空");
    } else {
      if (name.contains(SepConstant.UNDERLINE)) {
        return name;
      } else {
        return CharSequenceUtil.toUnderlineCase(name);
      }
    }
  }
}

4、拓展接口

UniqueExtend 



import com.baomidou.mybatisplus.core.conditions.AbstractWrapper;
import java.lang.reflect.Field;

public interface UniqueExtend {


  /**
   * 应用拓展 修改查询SQL
   * @author xgz
   * @date 2023/10/28
   * @param wrapper 默认的 warpper
   * @param entity 表对应的实体对象,注意这是一个空对象。
   *               但是可以通过反射获取 表的字段名等信息。
   * @param obj Bo对象,有各个前端传入的值
   * @param field 当前需要校验的字段,是Bo对象标注了@UniqueField 注解的字段。
   * @return com.baomidou.mybatisplus.core.conditions.Wrapper
  **/
  AbstractWrapper apply(AbstractWrapper wrapper, Object entity, Object obj, Field field);
}

5、拓展接口的一个实现类


import cn.hutool.core.text.CharSequenceUtil;
import com.baomidou.mybatisplus.core.conditions.AbstractWrapper;
import com.jxctjt.workflow.common.util.ReflectUtils;
import java.lang.reflect.Field;
import javax.annotation.Resource;
import javax.servlet.http.HttpServletRequest;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Component;

/**
 * 拓展加入appId条件判断
 *  **/
@Slf4j
@Component
public class UniqueExtendAppIdImpl implements UniqueExtend{

  @Resource
  private HttpServletRequest request;
  private final String THIRD_PART_NAME_HEADER = "app-id";
  /**
   * 应用拓展 修改查询SQL
   *
   * @param wrapper 默认的 warpper
   * @param entity  表对应的实体对象,注意这是一个空对象。 但是可以通过反射获取 表的字段名等信息。
   * @param obj     Bo对象,有各个前端传入的值
   * @param field   当前需要校验的字段,是Bo对象标注了@UniqueField 注解的字段。
   * @return com.baomidou.mybatisplus.core.conditions.Wrapper
   * @author xgz
   * @date 2023/10/28
   **/
  @Override
  public AbstractWrapper apply(AbstractWrapper wrapper, Object entity, Object obj, Field field) {
    // 首先从 obj中获取 appId,如果没有 再从 request的header 获取appId
    Field appIdField = ReflectUtils.getField(obj.getClass(), "appId");
    Object appId = null;
    if (appIdField != null){
      appId = ReflectUtils.invokeGetter(obj, appIdField.getName());
    }
    if (null == appId){
      String header = request.getHeader(THIRD_PART_NAME_HEADER);
      if (CharSequenceUtil.isNotBlank(header)){
        appId = header;
      }
    }
    if (appId == null){
      log.info("无appId!");
      return wrapper;
    }else {
      return wrapper.eq(true, "app_id", appId);
    }
  }
}

三、使用

1、controller层

  @PostMapping("/test")
  public Result test(@Validated(AddGroup.class) @RequestBody CategoryBo bo){
    log.info(bo.toString());
    //  逻辑
    return Result.ok("测试成功!");
  }

2、需要校验的Bo对象


import com.jxctjt.workflow.common.validate.group.AddGroup;
import com.jxctjt.workflow.common.validate.group.EditGroup;
import com.jxctjt.workflow.common.validate.group.anno.UniqueExtendAppIdImpl;
import com.jxctjt.workflow.common.validate.group.anno.UniqueField;
import com.jxctjt.workflow.common.validate.group.anno.UniqueType;
import com.jxctjt.workflow.domain.entity.ActCxCategoryEntity;
import com.jxctjt.workflow.mapper.ActCxCategoryMapper;
import io.swagger.annotations.ApiModel;
import io.swagger.annotations.ApiModelProperty;
import java.io.Serializable;
import javax.validation.constraints.NotNull;
import lombok.Data;

/**
 * @author xgz
 */
@Data
@UniqueType(entity = ActCxCategoryEntity.class, mapper = ActCxCategoryMapper.class, groups = {
    AddGroup.class, EditGroup.class})
@ApiModel("分类参数对象")
public class CategoryBo implements Serializable {

  private static final long serialVersionUID = 1L;

  @ApiModelProperty("分类id")
  @NotNull(message = "分类id不能为空", groups = {EditGroup.class})
  private String categoryId;

  /**
   * 分类名称
   */
  @UniqueField(message = "分类名称已存在")
 // @UniqueField(message = "分类名称已存在", extend = UniqueExtendAppIdImpl.class)
  @ApiModelProperty("分类名称")
  @NotNull(message = "分类名称不能为空", groups = {AddGroup.class, EditGroup.class})
  private String categoryName;

  /**
   * 图标
   */
  @ApiModelProperty("图标")
  private String icon;

  /**
   * 排序
   */
  @ApiModelProperty("排序")
  @NotNull(message = "排序不能为空", groups = {AddGroup.class, EditGroup.class})
  private Double sort;
}

 注意1:

@Data
@UniqueType(entity = ActCxCategoryEntity.class, mapper = ActCxCategoryMapper.class, groups = {
    AddGroup.class, EditGroup.class})
@ApiModel("分类参数对象")
public class CategoryBo implements Serializable {略}

注意2:

  /**
   * 分类名称
   */
  @UniqueField(message = "分类名称已存在")
 // @UniqueField(message = "分类名称已存在", extend = UniqueExtendAppIdImpl.class)
  @ApiModelProperty("分类名称")
  @NotNull(message = "分类名称不能为空", groups = {AddGroup.class, EditGroup.class})
  private String categoryName;

这是为了 校验 CategoryBo 接收的 categoryName 在数据库中不能重复。

3、测试

测试数据:

CASE 1:  更新场景 参数有表主键

参数:
  {
    "categoryId": "1f03b4e8bd920c6991cdc48d32b96cb40",
    "categoryName": "项目管理系统",
    "sort": 0
  }
  请求头: app-id :1691701502841065473
  结果:
  {
    "code": 500,
    "message": "......; default message [分类名称已存在]] ",
    "result": null,
    "success": false
  }

 
1、Bo 的是这样写时,不使用拓展
  /**
   * 分类名称
   */
  @UniqueField(message = "分类名称已存在")
 // @UniqueField(message = "分类名称已存在", extend = UniqueExtendAppIdImpl.class)
  @ApiModelProperty("分类名称")
  @NotNull(message = "分类名称不能为空", groups = {AddGroup.class, EditGroup.class})
  private String categoryName;
后台执行的Sql:
 SELECT COUNT( * ) FROM act_cx_category 
WHERE 
(category_name = '项目管理系统' 
    AND category_id <> '1f03b4e8bd920c6991cdc48d32b96cb40';
2、使用拓展
  /**
   * 分类名称
   */
  //@UniqueField(message = "分类名称已存在")
  @UniqueField(message = "分类名称已存在", extend = UniqueExtendAppIdImpl.class)
  @ApiModelProperty("分类名称")
  @NotNull(message = "分类名称不能为空", groups = {AddGroup.class, EditGroup.class})
  private String categoryName;

后台执行的sql

  SELECT COUNT( * ) 
FROM act_cx_category 
WHERE (category_name = '项目管理系统' 
AND category_id <> '1f03b4e8bd920c6991cdc48d32b96cb40'
AND app_id = '1691701502841065473');

CASE 2: 新增场景、参数没有表主键

参数:
  {
    "categoryName": "项目管理系统",
    "sort": 0
  }
  请求头: app-id :1691701502841065473
  1、不使用拓展
  /**
   * 分类名称
   */
  @UniqueField(message = "分类名称已存在")
  @ApiModelProperty("分类名称")
  @NotNull(message = "分类名称不能为空", groups = {AddGroup.class, EditGroup.class})
  private String categoryName;

 后台执行SQL

SELECT 
    COUNT( * ) 
FROM 
    act_cx_category 
WHERE 
    (category_name = '项目管理系统')
 2、使用拓展
  /**
   * 分类名称
   */
  @UniqueField(message = "分类名称已存在", extend = UniqueExtendAppIdImpl.class)
  @ApiModelProperty("分类名称")
  @NotNull(message = "分类名称不能为空", groups = {AddGroup.class, EditGroup.class})
  private String categoryName;

后台执行的sql

SELECT 
    COUNT( * ) 
FROM 
    act_cx_category 
WHERE 
    (category_name = '项目管理系统' AND app_id = '1691701502841065473')

四、说明

本文使用2个自定义注解+1个扩展接口,实现 使用注解方式,校验数据库的字段是否重复。

2个注解需要搭配使用。拓展接口的实现类可以自己定义,实现符合自己逻辑的校验规则。

  • 1
    点赞
  • 5
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值