AOP数据唯一性校验

数据唯一性校验

介绍

在新增或修改操作的时候,我们经常需要校验数据是否已经存在,每次都是同样的逻辑,无非就是表名和查询条件的字段名不相同,代码显得很冗余。

AOP校验数据在新增或修改是否已经存在:

  • 注解参数:表名,字段列表,新增还是修改,响应信息
    • 新增:select count(*) from 表名 where 遍历字段列表 = 参数列表
    • 修改:select count(*) from 表面 where 遍历字段列表 = 参数列表 and id != 修改数据id

通过切面拼接表名和查询条件,执行查询,如果返回结果大于0,则直接响应信息。

注解

检查数据唯一性注解:

1
2
3
4
5
6
7
8
9
10
11
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface CheckUnique {
String table() default "";

CheckType type() default CheckType.ADD;

String message() default "数据已存在";

KeyValue[] keyValues();
}

子注解:用于数据库字段和对象属性名映射

1
2
3
4
5
6
7
8
9
10
11
12
13
@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
public @interface KeyValue {

// 混用名(数据库字段名或者属性名)
String value();

// 数据库字段名
String key() default "";

// 是否主键
boolean isPK() default false;
}

枚举类:用于区分当前为新增还是修改操作

1
2
3
4
5
6
7
8
9
10
11
12
public enum CheckType
{
/**
* 新增
*/
ADD,

/**
* 修改
*/
UPDATE,
}

切面

如果项目使用了Mybatis-Plus框架,则可以结合@TableName@TableId注解使用,注解参数可以省略表名和主键。

该切面使用了工具类:

  • 驼峰转下划线
  • 下划线转驼峰
  • SQL注入风险校验
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
/**
* 用于判断数据库中是否存在重复数据的切面类。
* 可以结合Mybatis-Plus注解使用
* -- @TableName
* -- @TableId
*/
@Aspect
@Component
public class CheckUniqueAspect {

@Autowired
private GeneralMapper generalMapper;
@Before("@annotation(checkUnique)")
public void checkDatabaseForExistence(JoinPoint joinPoint, CheckUnique checkUnique) {

KeyValue[] keyValues = checkUnique.keyValues();
if (keyValues.length == 0) {
throw new IllegalArgumentException("至少包含一个查询条件!");
}

// 获取方法参数
Object[] args = joinPoint.getArgs();
if (args.length == 0) {
throw new IllegalArgumentException("方法应该至少有一个参数!");
}
Object entity = args[0];
Class<?> entityClass = entity.getClass(); // 获取entity对象的类
HashMap<String, Object> map = new HashMap<>();
// 判断检查类型,修改需包含主键
if (checkUnique.type() == CheckType.UPDATE) {
// 检查keyValues是否有主键
boolean hasPrimaryKey = Arrays.stream(keyValues).anyMatch(KeyValue::isPK);
// 如果没有,则去@TableId注解获取
if (!hasPrimaryKey) {
// 获取实体类中的主键属性
Field idField = findFieldWithAnnotation(entityClass, TableId.class);
// 获取字段名
String idFieldName = idField.getName();
// 转换为下划线命名
String underFieldName = StringUtils.toUnderScoreCase(idFieldName);
// 设置字段为可访问(如果它不是public的)
idField.setAccessible(true);
try {
// 获取字段的值
Object idValue = idField.get(entity);
// 防止SQL注入
SqlUtil.filterKeyword(idValue.toString());

// 放入map
map.put(underFieldName, idValue);
} catch (IllegalAccessException e) {
// 处理访问异常
e.printStackTrace();
throw new IllegalArgumentException("修改类型注解数组需要包含主键!");
}
}
}


// 获取注解中的表名
String table = checkUnique.table();
// 如果为空,则从类中的@Table注解中获取
if (StringUtils.isEmpty(table)) {
// 判断entityClass上是否有@TableName
if (entityClass.isAnnotationPresent(TableName.class)) {
// 获取entityClass上的@EntityAnnotation注解实例
TableName annotation = entityClass.getAnnotation(TableName.class);
table = annotation.value(); // 获取注解值
} else {
throw new IllegalArgumentException("需要给定表名!");
}
}


// 构建查询条件
StringBuilder queryBuilder = new StringBuilder();
for (KeyValue keyValue : keyValues) {

String valueExpr = keyValue.value();
if (StringUtils.isEmpty(valueExpr)) {
throw new IllegalArgumentException("@KeyValue注解参数不能为空!");
}

String[] parts = valueExpr.split("\\.");
String fieldName = parts.length == 2 ? parts[1] : parts[0];

String key = StringUtils.isEmpty(keyValue.key()) ? fieldName : keyValue.key();

// 转下划线
key = StringUtils.toUnderScoreCase(key);

// 从实体类中获取属性值
Object value = getValue(entity, fieldName);

// 防止SQL注入
SqlUtil.filterKeyword(value.toString());

if (ObjectUtil.isEmpty(value)) {
throw new IllegalArgumentException("属性值不能为空!");
}

if (keyValue.isPK()) {
map.put(key, value);
} else {
queryBuilder.append(key).append(" = ").append("'").append(value).append("'");
}
if (keyValues.length > 1 && !keyValues[keyValues.length - 1].equals(keyValue)) {
queryBuilder.append(" AND ");
}
}

// 拼接主键查询条件
if (!map.isEmpty()) {
for (Map.Entry<String, Object> entry : map.entrySet()) {
queryBuilder.append(" AND ");
queryBuilder.append(entry.getKey()).append(" != ").append("'").append(entry.getValue()).append("'");
}
}

// 执行查询
String query = queryBuilder.toString();
System.out.println("==================" + query + "====================");
int count = generalMapper.getCount(table, query);
// 如果数据库存在数据,则抛出异常信息
if (count > 0) {
throw new RuntimeException(checkUnique.message());
}

}

/**
* 获取实体类中指定表达式的值
* @param entity
* @param fieldName
* @return
*/
private Object getValue(Object entity, String fieldName) {

if (StringUtils.isEmpty(fieldName)) {
throw new IllegalArgumentException("无效的表达格式: " + fieldName);
}
// 转驼峰
Field field = findField(entity.getClass(), StringUtils.toCamelCase(fieldName));

if (!field.isAccessible()) {
field.setAccessible(true);
}

try {
Object value = field.get(entity);
// 时间类型需要格式化处理一下
if (field.getType() == Date.class) {
value = DateUtils.parseDateToStr("yyyy-MM-dd HH:mm:ss", (Date) value);
}
return value;
} catch (IllegalAccessException e) {
throw new RuntimeException("从访问字段失败: " + fieldName);
}
}

/**
* 递归父类查找字段是否包含属性名
* @param clazz
* @param fieldName
* @return
*/
private Field findField(Class<?> clazz, String fieldName) {
if (clazz == null) {
throw new IllegalArgumentException("找不到字段: " + fieldName);
}
// 首先尝试在当前类中查找字段
try {
return clazz.getDeclaredField(fieldName);
} catch (NoSuchFieldException e) {
// 字段未在当前类中找到,则递归查找父类
return findField(clazz.getSuperclass(), fieldName);
}
}

/**
* 递归父类查找属性是否包含给定注解
* @param clazz
* @param annotationClass
* @return
*/
private Field findFieldWithAnnotation(Class<?> clazz, Class<? extends Annotation> annotationClass) {
Field foundField = null;
// 遍历当前类的所有字段
for (Field field : clazz.getDeclaredFields()) {
// 如果字段上有指定的注解,保存这个字段
if (field.isAnnotationPresent(annotationClass)) {
foundField = field;
break;
}
}
// 如果当前类没有找到,递归查找父类
if (foundField == null) {
Class<?> superclass = clazz.getSuperclass();
if (superclass != null) {
foundField = findFieldWithAnnotation(superclass, annotationClass);
} else {
throw new IllegalArgumentException("修改类型注解数组需要包含主键!");
}
}
return foundField;
}
}

执行Mapper:

1
2
3
public interface GeneralMapper {
int getCount(@Param("tableName") String tableName, @Param("queryCondition") String queryCondition);
}

Mapper.xml:

1
2
3
4
5
6
7
8
9
10
11
<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE mapper
PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
"http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="com.tsy.general.mapper.GeneralMapper">

<select id="getCount" parameterType="String" resultType="Integer">
select count(*) from ${tableName} where ${queryCondition}
</select>

</mapper>

使用

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
// 新增
@Override
@CheckUnique(table = "tsy_clinometer_data", keyValues = {
@KeyValue(key = "time", value = "tsyClinometerData.time"),
@KeyValue(key = "clinometer_node_id", value = "tsyClinometerData.clinometerNodeId")}, message = "新增失败,该测斜数据已存在!")
public int insertTsyClinometerData(TsyClinometerData tsyClinometerData) {
return tsyClinometerDataMapper.insertTsyClinometerData(tsyClinometerData);
}

// 修改
@Override
@CheckUnique(table = "tsy_clinometer_data", keyValues = {
@KeyValue(key = "time", value = "tsyClinometerData.time"),
@KeyValue(key = "clinometer_node_id", value = "tsyClinometerData.clinometerNodeId"),
@KeyValue(key = "id", value = "tsyClinometerData.id", isPK = true)}, type = CheckType.UPDATE, message = "修改失败,该测斜数据已存在!")
public int updateTsyClinometerData(TsyClinometerData tsyClinometerData) {
return tsyClinometerDataMapper.updateTsyClinometerData(tsyClinometerData);
}


// 如果有MP注解,则可以简化
@PutMapping
@CheckUnique(keyValues = {@KeyValue("name")}, type = CheckType.UPDATE, message = "修改失败,名称已存在")
public AjaxResult update(@RequestBody TsyTest tsyTest) {
boolean update = tsyTestService.updateById(tsyTest);
return toAjax(update);
}

@PostMapping
@CheckUnique(keyValues = {@KeyValue("name")}, message = "新增失败,名称已存在")
public AjaxResult add(@RequestBody TsyTest tsyTest) {
boolean save = tsyTestService.save(tsyTest);
return toAjax(save);
}