Duplicate entry 'xxx' for key 'xxxx'
平时我们遇到 mysql 报错的时候很难处理,例如字段重复了,破坏了完整性约束,但是我们又不可能每个唯一字段都去进行查询,看看是不是重复了,这样相当于写死了代码。
那么如何动态的去知道那个信息被占用,或者重复了呢? 这里就需要用到反射的原理
首先我们得学会捕获全局异常 :
使用 @RestControllerAdvice
环绕增强 ,然后再使用 @ExceptionHandler
注解,在某个方法上面给指定的异常类进行注解, Speingboot 会自动将异常分发给你的方法进行处理。
这里的
@ResponseHandler
注解是统一信息处理注解,可以在我的文章 https://blog.csdn.net/qq_31254489/article/details/119772338 中去学习配置,如果你不想用,可以删掉。
@RestController
@ResponseHandler
@RestControllerAdvice
@Api(tags = "統一错误请求控制")
public class MyErrorController implements ErrorController {
// 统一sql异常处理类
@Autowired
SqlExceptionHandler sqlExceptionHandler;
// sql 报错处理
@ExceptionHandler(value = SQLException.class)
@ResponseStatus
public String sqlError(SQLException e) {
// 我们交给异常处理类去处理
return sqlExceptionHandler.handle(e);
}
}
具体流程如下
完整性约束报错处理
一般就是插入的时候和已经有的数据重复了
接下来复制粘贴下面2个类,并要遵守以下规则
- 数据库的约束名称必须遵守
表名.表名_键1_键2_键3_xxx_uindex
例如user.user_name_unidex
意思就是 user 表下的 name 的唯一约束。 - 实体类的名称,还有属性,必须和数据库中的一致,mysql 必须遵守下划线命名规则, java 则可以使用 驼峰或者下划线。
- 使用
@HandleSqlException
注解去方法上面标注需要处理的具体异常。 例如 下面的integrityConstraint
方法。
/**
* sql 错误异常处理
*
* @author enncy
*/
@Component
public class SqlExceptionHandler {
/**
* 分发异常处理
* @return: void
*/
public String handle(SQLException e) {
// 获取当前类的方法
Optional<Method> first = Arrays.stream(SqlExceptionHandler.class.getDeclaredMethods())
// 寻找有 HandleSqlException 注解的方法
.filter(m -> m.isAnnotationPresent(HandleSqlException.class))
// 寻找和参数 e 的类相等的 HandleSqlException
.filter(m -> m.getAnnotation(HandleSqlException.class).value() == e.getClass())
.findFirst();
// 如果存在,则分发给指定的方法
if( first.isPresent()){
try {
return (String) first.get().invoke(this, e);
} catch (IllegalAccessException | InvocationTargetException exception) {
return "服务器内部出现异常";
}
}else{
return "服务器内部出现异常";
}
}
/**
* 完整性约束被破坏处理
* @return: java.lang.String
*/
@HandleSqlException(SQLIntegrityConstraintViolationException.class)
public String integrityConstraint(SQLException e) {
// 匹配数据库报错信息
String regex = "Duplicate entry '(.*?)' for key '(.*?)\\..*?_(.*?)_uindex'";
Matcher matcher = Pattern.compile(regex).matcher(e.getMessage());
// 如果存在信息
if (matcher.find()) {
// 获取被占用的值
String value = matcher.group(1);
// 获取数据库表
String table = matcher.group(2);
// 获取发生冲突的约束键
String keys = matcher.group(3);
// 扫描实体类
List<Class<?>> scan = ClassScanner.scan("cn.enncy.funny.entity");
// 寻找冲突的字段的注解描述
String description = scan.stream()
.filter(BaseEntity.class::isAssignableFrom)
.filter(c -> c.getName().toLowerCase().contains(table))
.map(c -> {
// 获取实体类的属性值
Field[] declaredFields = c.getDeclaredFields();
for (Field declaredField : declaredFields) {
// 驼峰转下划线,这里的 humpToUnderline 方法参考文章 : https://blog.csdn.net/qq_31254489/article/details/115842821
String name = StringUtils.humpToUnderline(declaredField.getName());
// 如果属性包含在 keys 的值里面
if (keys.contains(name)) {
// 返回属性上面的 注解信息
return declaredField.getAnnotation(ApiModelProperty.class).value();
}
}
return "";
}).findFirst().orElse("");
return description + " " + value + " 已经被占用";
}
return "信息已经被占用";
}
}
/**
* @author enncy
*/
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface HandleSqlException {
Class<? extends SQLException> value();
}
然后修改完整性约束处理的里面的包路径 cn.enncy.funny.entity
, 这个需要你改到自己项目的实体类包下,也就是POJO类所在的包。
测试实例
这是我的实体类 , 在 ‘cn.enncy.xxx.entity’ 包下,所以把包扫描信息改成 ClassScanner.scan("cn.enncy.xxx.entity");
然后把项目跑起来,测试占用信息。
邮箱字段被占用
报错 : Cause: java.sql.SQLIntegrityConstraintViolationException: Duplicate entry 'xxxxxxxxxx@qq.com' for key 'user.user_email_uindex'
响应
账号被占用
报错 : Cause: java.sql.SQLIntegrityConstraintViolationException: Duplicate entry 'xxxxx' for key 'user.user_account_uindex'
原理
大概的就是利用 mysql 报错的信息, 然后进行字符串处理。
思路 :
- 全局获取 mysql 的报错信息
- 对报错信息进行正则表达式判断,取出有用的信息。
- 利用反射原理找出和报错信息所对应的实体类
- 反射找出实体类的字段和注释信息
- 最后返回统一信息