统一验证数据表唯一值

76 阅读4分钟

统一验证数据表的唯一值

在开发过程中,经常会遇到一张数据表中的某个字段或多个字段的组合是唯一值。通常的做法是:

  1. 创建唯一索引或唯一约束:在创建数据表时,为一个或多个列设置唯一约束(UNIQUE constraint),数据库系统会自动拒绝插入或更新任何违反唯一性的记录。
  2. 应用程序逻辑:在应用程序层面上实现检查逻辑,在插入或更新记录之前先查询是否已经存在相同的记录。如果存在,则根据业务规则决定是更新现有记录还是拒绝操作。
  3. 使用触发器:创建数据库触发器来拦截试图插入或更新的行,并执行额外的检查以确保唯一性。触发器可以在尝试进行更改时自动运行特定的SQL代码。
  4. 数据清洗:对于已有的数据表,可能需要先进行一次全面的数据清洗工作,找出并处理所有重复项。可以编写脚本来查找和合并或删除重复的记录。
  5. 定期审核:定期对数据表进行审核,确保没有新的重复项产生。需要使用专门的工具或编写自定义查询来进行这样的检查。
  6. 使用存储过程:将复杂的验证逻辑封装进存储过程中,当需要插入或更新记录时调用这些存储过程。

在应用程序逻辑中,大部分的做法也是在添加方法中使用if语句判断是否存在重复,这种做法存在冗余的代码,而且在每张数据表的插入中都需要进行判断。可以通过自定义验证器的方式,在需要验证唯一性的字段上标注注解,由验证器进行唯一性的验证。这种方式能够很好的实现代码复用和代码简洁

实现方式

引入依赖

 <dependency>
     <groupId>org.springframework.boot</groupId>
     <artifactId>spring-boot-starter-validation</artifactId>
 </dependency>

单个字段唯一性校验

定义注解

 @Documented
 @Target({ElementType.FIELD})
 @Retention(RetentionPolicy.RUNTIME)
 // 指定校验类
 @Constraint(validatedBy = {UniqueValidator.class})
 public @interface Unique {
     // 查询数据库所调用的class文件
     Class<? extends IService<?>> service();
 ​
     /**
      * 字段名称
      *
      * @return 字段名
      */
     String field();
 ​
     /**
      * 校验失败时的错误信息
      *
      * @return str
      */
     String message();
 ​
     Class<?>[] groups() default {};
     Class<? extends Payload>[] payload() default {};
 }

实现自定义校验类

 @Slf4j
 @Component
 public class UniqueValidator implements ConstraintValidator<Unique, String> {
     private Unique unique;
     @Resource
     private ApplicationContext applicationContext;
 ​
     /**
      * 主要做一些初始化操作,它的参数是使用到的注解,可以获取到运行时的注解信息
      *
      * @param unique 自定义注解
      */
     @Override
     public void initialize(Unique unique) {
         this.unique = unique;
     }
 ​
     /**
      * 要实现的校验逻辑,被注解的对象会传入此方法中
      *
      * @param str                        需要检验的值
      * @param constraintValidatorContext 用于在执行自定义校验逻辑时向用户提供反馈
      * @return true|false
      */
     @Override
     public boolean isValid(String str, ConstraintValidatorContext constraintValidatorContext) {
         String wrapperSql = String.format("%s=?", unique.field());
         IService<?> service = applicationContext.getBean(unique.service());
         return !service.exists(QueryWrapper.create().where(wrapperSql, str));
     }
 }

多个字段唯一性校验

 @Documented
 @Target({ElementType.TYPE})
 // 允许重复注解
 @Repeatable(UnionUniques.class)
 @Retention(RetentionPolicy.RUNTIME)
 @Constraint(validatedBy = {UnionUniqueValidator.class})
 public @interface UnionUnique {
     /**
      * 查询数据库的服务接口
      *
      * @return 服务接口
      */
     Class<? extends IService<?>> service();
 ​
     /**
      * 组合字段
      *
      * @return 组合字段名称
      */
     String[] fields();
 ​
     /**
      * 错误提示信息
      *
      * @return 错误提示信息
      */
     String message();
 ​
     Class<?>[] groups() default {};
 ​
     Class<? extends Payload>[] payload() default {};
 }
 @Documented
 @Target({ElementType.TYPE})
 @Retention(RetentionPolicy.RUNTIME)
 public @interface UnionUniques {
     UnionUnique[] value();
 }

自定义校验方式

 @Slf4j
 @Component
 public class UnionUniqueValidator implements ConstraintValidator<UnionUnique, Object> {
     private UnionUnique unionUnique;
 ​
     @Resource
     private ApplicationContext applicationContext;
 ​
     @Override
     public void initialize(UnionUnique unionUnique) {
         this.unionUnique = unionUnique;
     }
 ​
     @Override
     public boolean isValid(Object obj, ConstraintValidatorContext constraintValidatorContext) {
         String[] fields = unionUnique.fields();
         StringBuilder wrapperSql = new StringBuilder("1=1");
         List<Object> vals = new ArrayList<>();
 ​
         for (String fieldStr : fields) {
             // 使用反射机制获取字段对象
             Field field = ReflectionUtils.findField(obj.getClass(), fieldStr);
             Assert.notNull(field, "field " + fieldStr + " not found");
             ReflectionUtils.makeAccessible(field);
 ​
             // 拼接sql,值使用占位符方式,防止SQL注入
             wrapperSql.append(String.format(" and %s=? ", field.getName()));
 ​
             // 获取字段值
             vals.add(ReflectionUtils.getField(field, obj));
         }
 ​
         IService<?> service = applicationContext.getBean(unionUnique.service());
         // 调用存储库方法来检查唯一性(需要根据具体情况实现)
         return !service.exists(QueryWrapper.create().where(wrapperSql.toString(), vals.toArray()));
     }
 }

使用实例

标注唯一性校验的entity类

 @Data
 @SuperBuilder
 @NoArgsConstructor
 @AllArgsConstructor
 public class SysAccount {
     @NotBlank(message = "用户名不允许为空", groups = {Add.class})
     @Unique(field = "username", service = ISysAccountService.class, message = "用户名已存在")
     private String username;
 ​
     private String avatar;
 ​
     @Email(message = "邮箱格式不正确")
     @NotBlank(message = "邮箱不允许为空", groups = {Add.class})
     private String email;
     private String realName;
     private String remark;
 }
 @Data
 @Builder
 @NoArgsConstructor
 @AllArgsConstructor
 @UnionUniques({
         @UnionUnique(service = ISysMenuService.class, fields = {"url", "method"}, message = "菜单请求地址和请求方式组合已存在"),
         @UnionUnique(service = ISysMenuService.class, fields = {"icon", "type"}, message = "菜单图标组合已存在")
 })
 public class SysMenuVO implements Serializable {
     @Serial
     private static final long serialVersionUID = 1L;
 ​
     /**
      * 父ID
      */
     @Schema(description = "父节点")
     private Long parentId;
 ​
     /**
      * 图标
      */
     @Schema(description = "图标")
     private String icon;
 ​
     /**
      * 名称
      */
     @Schema(description = "名称")
     @Unique(field = "name", service = ISysMenuService.class, message = "菜单名称已存在")
     private String name;
 ​
     /**
      * 权限编码
      */
     @Schema(description = "权限编码")
     @Unique(field = "code", service = ISysMenuService.class, message = "菜单编码已存在")
     private String code;
 ​
     /**
      * 请求路径
      */
     @Schema(description = "请求路径")
     private String url;
 ​
     /**
      * 请求方式
      */
     @Schema(description = "请求方式")
     private String method;
 ​
     /**
      * 类型
      */
     @Schema(description = "菜单类型")
     private Integer type = 1;
 ​
     /**
      * 排序
      */
     @Schema(description = "菜单排序")
     private Integer sort = 99;
 ​
     /**
      * 外链地址
      */
     @Schema(description = "外链地址")
     private String redirect = "";
 ​
     /**
      * 是否隐藏
      */
     @Schema(description = "是否隐藏")
     private Integer hidden = 1;
 ​
     /**
      * 描述信息
      */
     @Schema(description = "描述信息")
     private String description;
 }

在controller层进行数据的校验

 @RestController
 @RequestMapping("/account")
 public class SysAccountController {
     @Resource
     private ISysAccountService service;
 ​
     @PostMapping()
     public Long save(@Validated @RequestBody SysAccount account) {
         return service.addSysAccount(account);
     }
 }