今天我们来讲一下MES的外键翻译。什么叫外键翻译呢?在很多时候我们设计表的时候往往会有很多关联的ID字段,很多时候在前端展示的时候需要通过ID去读取对应表的字段,
例如:user_id,我们在前端展示的时候需要的是user_name。一般我们有两种方式: 1.通过关联查询把用户表的user_name查询出来; 2.通过添加冗余字段把user_name保存 第一种方法往往会带来很多额外的编程工作量,第二种冗余字段会带来一个问天,关联表信息发生变化要动态去更新主表信息。
由此我们是不是通过一些手段,可以在接口返回是字段把关联ID字段,自动转换成关联表所需要的字段呢?
我们可以通过spring boot的AOP+注解实现。
下面我们来详细讲讲如何实现:
1. 新增注解类
TableDict注解, table为要关联的表名,key为外键要关联表的字段名,columns为要查询关联表的字段名
@Target(ElementType.FIELD)
@Retention(RetentionPolicy.RUNTIME)
public @interface TableDict {
/**
* 关联的表名
* @return string
*/ String table();
/**
* 关联的字段
* @return string
*/ String key();
DictColumn[] columns();
}
DictColumn注解,name为关联表的字段名成,alias为返回到前端的名称。
public @interface DictColumn {
/**
* 字段名
* @return
*/ String name();
/**
* 别名
* @return
*/ String alias();
}
备注: 这里为什么还要加多一个DictColumn的注解呢,主要是考虑到有时候查询关联表不止是一个字段名,可能会有多个,使用DictColumn[]就可以定义要返回的多个字段名。
2. 实体类标记字段
这里我以查询多个字段为例:
public class CrmSalesDetail implements Serializable {
...
@TableDict(table = "base_material", key = "id" , columns = {
@DictColumn(name ="material_code", alias = "materialCode"),
@DictColumn(name = "material_name", alias = "materialName")
})
private Long materialId;
...
}
说明:以上是销售单明细的实体类,其中
materialId为采购物料的id,在前端表格中需要显示物料编码和物料名称
3. 实现AOP拦截
这里定义了一个对Controller类返回值拦截,并对返回内容进行加工处理。代码逻辑如下:
- 使用AOP对
Controller进行拦截 - 如果返回数据是List或者Page类型,则捕获数据
- 使用发射机制获取对象所有字段,判断字段是否包含
@TableDict - 通过字段值和
@TableDict注解的信息查询表获取DictColumn注解定义的字段 - 使用Jackson工具类把查询字段和值合并,返回给前端
以下是代码实现的代码片段,如需详细代码可以从Gitee下载源码查看。
TableDictAspect代码路径:hgyc-core/src/main/java/com/hgyc/mom/core/aspectj/TableDictAspect.java
拦截Controller
/**
* 定义切点Pointcut
*/@Pointcut("execution(public * com.hgyc.mom..*.*Controller.*(..))")
public void executeService() {
}
拦截定义的返回类型 这里可以根据不同的需求,去定义需要拦截的返回类型
private Object parseDictText(Object result) {
//这里只翻译分页和List的返回值
if (result instanceof Result) {
Result ajaxResult = (Result) result;
List<Object> dataList = null;
if (Objects.nonNull(ajaxResult.getData())) {
if (ajaxResult.getData() instanceof Collection<?>) {
dataList = (List<Object>) ajaxResult.getData();
} else if (ajaxResult.getData() instanceof IPage) {
dataList = ((Page) ajaxResult.getData()).getRecords();
}
}
if (!CollectionUtils.isEmpty(dataList)) {
//先判断是否有转化字段
if (!this.checkHasDict(dataList.get(0))) {
return ajaxResult;
}
ArrayNode arrayNode = objectMapper.createArrayNode();
for (Object item : dataList) {
JsonNode resultNode = processObjectDictFields(item);
arrayNode.add(resultNode);
}
List<Object> objectList = objectMapper.convertValue(arrayNode, new TypeReference<>() {
});
if (ajaxResult.getData() instanceof IPage) {
((IPage) ajaxResult.getData()).setRecords(objectList);
} else {
ajaxResult.setData(objectList);
}
}
return ajaxResult;
} else {
return result;
}
}
判断类型是否包含 TableDic注解
/**
* 检测是否有字典注解
* @param obj
* @return
*/private boolean checkHasDict(Object obj) {
boolean flag = false;
//判断是否基础类型或包装类型
if(ClassUtils.isPrimitiveOrWrapper(obj.getClass())){
return false;
}
List<Field> allFields = getAllFields(obj.getClass());
for (Field field : allFields) {
TableDict tableDict = field.getAnnotation(TableDict.class);
if (tableDict != null) {
flag = true;
return flag;
}
}
return flag;
}
根据TableDict注解查询关联表数据
/**
* 处理对象中的@TableDict注解字段
*/
private JsonNode processObjectDictFields(Object obj) {
AtomicReference<JsonNode> resultNode = new AtomicReference<>();
// 获取对象所有字段(包括父类)
List<Field> allFields = getAllFields(obj.getClass());
// 按@TableDict分组(优化批量查询)
Map<String, List<DictQueryInfo>> tableQueryMap = new HashMap<>();
// 第一遍扫描:收集需要翻译的字段信息
for (Field field : allFields) {
TableDict tableDict = field.getAnnotation(TableDict.class);
if (tableDict != null) {
try {
field.setAccessible(true);
Object fieldValue = field.get(obj);
if (fieldValue != null) {
String cacheKey = buildCacheKey(tableDict.table(), fieldValue.toString());
tableQueryMap
.computeIfAbsent(tableDict.table(), k -> new ArrayList<>())
.add(new DictQueryInfo(cacheKey, tableDict, fieldValue, field));
}
} catch (IllegalAccessException e) {
throw new RuntimeException("反射访问字段失败: " + field.getName(), e);
}
}
}
// 将对象转为JSON树
ObjectNode jsonNode = objectMapper.valueToTree(obj);
// 批量处理每个表的翻译需求
tableQueryMap.forEach((tableName, queryInfos) -> {
// 1. 先尝试从Redis批量获取
Map<String, Map<String, String>> cachedData = batchGetFromRedis(
queryInfos.stream().map(DictQueryInfo::cacheKey).collect(Collectors.toList())
);
// 2. 找出需要查数据库的key
List<DictQueryInfo> needDbQuery = queryInfos.stream()
.filter(info -> !cachedData.containsKey(info.cacheKey()))
.collect(Collectors.toList());
// 3. 批量查询数据库
if (!needDbQuery.isEmpty()) {
Map<String, Map<String, String>> dbData = batchQueryFromDatabase(tableName, needDbQuery);
cachedData.putAll(dbData);
}
// 4. 动态添加字段到对象
queryInfos.forEach(info -> {
Map<String, String> translated = cachedData.get(info.cacheKey());
if (translated != null) {
// 添加动态字段
translated.forEach(jsonNode::put);
}
});
});
return jsonNode;
}
这里考虑到性能问题,加上了Redis缓存。
使用Jackson把翻译字段合并返回前端
List<Object> objectList = objectMapper.convertValue(arrayNode, new TypeReference<>() {
});
if (ajaxResult.getData() instanceof IPage) {
((IPage) ajaxResult.getData()).setRecords(objectList);
} else {
ajaxResult.setData(objectList);
}
效果
返回前端数据,通过chrome开发者工具,获取到接口返回数据已经可以看到刚才我们定义的materialCode和materialName字段。
本文源码已上传Gitee 开源项目地址:
欢迎在评论区分享你的技术选型经验,或对本文方案的改进建议!
关注公众号「慧工云创」