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