在我们日常的开发中,我们经常会遇到这种问题。有一张表,这张表中有一些字段在表中是唯一存在的,比如:我们的邮箱字段email、手机号字段phone又或者要求名称name不能够重复等等,这些字段要求在数据表中是唯一存在的。我们想到的方法是在数据表创建时给这些唯一性约束的条件加上唯一性索引。如:
ALTER TABLE users
ADD CONSTRAINT unique_email UNIQUE (email);
这段语句就说明我们在给users表的email字段加上了唯一性索引,并且指定这个索引的名称为unique_email。
但是这样是很不友好的,我们出错之后是由数据库给我们兜底的,并且返回的错误信息很不友好。如:
这段报错信息说明我们在插入数据库时,name名称字段违反了唯一性索引的约束,所以报错。当然,我们也能想到一些方法来进行修复,比如我创建一个全局的异常通知类,然后再这个类中统一拦截我们的异常并进行处理
@ExceptionHandler(DuplicateKeyException.class)
public Result<Object> error(DuplicateKeyException e) {
String message = Objects.requireNonNull(e.getRootCause()).getMessage(); // 获取底层 SQL 异常信息
if (message != null && message.contains("Duplicate entry")) {
// 例如:Duplicate entry '张三' for key 'user.uk_name'
// 你可以用正则提取索引名
Pattern pattern = Pattern.compile("for key '(.+?)'");
Matcher matcher = pattern.matcher(message);
if (matcher.find()) {
String indexName = matcher.group(1);
log.error("捕获到数据库唯一键冲突异常:{}", indexName);
return Result.error(501,"唯一索引冲突:" + indexName);
}
}
return Result.error(501, "该记录已存在,请勿重复操作");
}
现在,我们抛出异常的提示信息比之前清晰了一点。如:
但是,这任然不是我们想要的结果,这样我们一次请求中只能知道一次唯一性索引的报错,如果有多个唯一性约束字段我们可能要等多次才能得到最终结果。并且,返回的报错信息也是我们唯一索引的约束名称。
说到底,我们不应该把数据库的兜底直接作为我们处理唯一性约束的方法。我们应该在插入或修改之前就直接判断出哪些字段违反了唯一性索引的约束。
这种也很好解决,我们在插入或修改之前先进行一次查询。看看要操作的参数有没有违反唯一性索引的约束。
如:对名称name这个字段做唯一性查询;
@PostMapping("/insert")
public String insert(@RequestBody User user) {
Integer count=userMapper.getExistsByName(user.getName(),null);
if(count>0){
throw new ResultException(501,"用户名已存在,请重新输入");
}
userMapper.insert(user);
return "success";
}
@PostMapping("/updateById")
public String updateById(@RequestBody User user) {
Integer count=userMapper.getExistsByName(user.getName(),user.getId());
if(count>0){
throw new ResultException(501,"用户名已存在,请重新输入");
}
userMapper.updateByPrimaryKey(user);
return "success";
}
在新增或者修改之前直接根据对应的字段来进行查询。
相应的mapper接口如下:
<select id="getExistsByName" resultType="java.lang.Integer">
select count(*) from user where
is_delete=0 and
name=#{name}
<if test="id != null">and id!=#{id}</if>
</select>
现在,我们对于单个字段的唯一性索引也已经能够实现了。但是,这也只是对于单个字段的约束查询,如果,是多个字段呢?并且这多个唯一性约束的字段是可变的呢。我们可以想到的是,我们还是使用这种先查询在新增/修改的方法,只不过是我们要做的通用化。
我们可以先自定义两个注解,用来收集需要加唯一性约束的字段。
如下:
收集单个唯一性字段
@Repeatable(UniqueFields.class)
@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
public @interface UniqueField {
/**
* Java实体属性名(必须)
*/
String property();
/**
* 数据库列名(可选)
* - 为空时自动将属性名转换为下划线格式
*/
String column() default "";
/**
* 字段中文名称(用于错误消息)
*/
String name();
}
收集一组唯一性字段
@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
public @interface UniqueFields {
UniqueField[] value();
}
我们收集了这些字段之后,可以进行一些处理了。我们最后还是要操作数据库表的,只不过我们把这些需要处理的唯一性字段都收集起来方便统一处理,在XML的SQL语句中,我们会使用foreach循环查询并且使用到了动态SQL的拼接。
定义一个工具类来实现我们收集到的唯一性索引的数据过滤
@Slf4j
@Component
public class UniqueValidator {
/**
* 字段配置信息
*/
private static class FieldConfig {
final String propertyName; // Java属性名
final String columnName; // 数据库列名
final String chineseName; // 中文名称
FieldConfig(String propertyName, String columnName, String chineseName) {
this.propertyName = propertyName;
// 智能列名处理:如果未指定列名,则自动转换驼峰为下划线
if (StringUtils.hasText(columnName)) {
this.columnName = columnName;
} else {
this.columnName = camelToSnake(propertyName);
}
this.chineseName = chineseName;
}
/**
* 驼峰命名转下划线命名
*/
private String camelToSnake(String camelCase) {
if (camelCase == null || camelCase.isEmpty()) {
return camelCase;
}
StringBuilder result = new StringBuilder();
char[] chars = camelCase.toCharArray();
for (int i = 0; i < chars.length; i++) {
char c = chars[i];
if (Character.isUpperCase(c)) {
// 在第一个字符前不加下划线
if (i