从0到1构建MES系统9-外键翻译

146 阅读4分钟

今天我们来讲一下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类返回值拦截,并对返回内容进行加工处理。代码逻辑如下:

  1. 使用AOP对Controller进行拦截
  2. 如果返回数据是List或者Page类型,则捕获数据
  3. 使用发射机制获取对象所有字段,判断字段是否包含@TableDict
  4. 通过字段值和@TableDict注解的信息查询表获取DictColumn注解定义的字段
  5. 使用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);  
}

效果

Pasted image 20250606182208.png

Pasted image 20250606182258.png 返回前端数据,通过chrome开发者工具,获取到接口返回数据已经可以看到刚才我们定义的materialCode和materialName字段。

本文源码已上传Gitee 开源项目地址

欢迎在评论区分享你的技术选型经验,或对本文方案的改进建议!

关注公众号「慧工云创」

扫码_搜索联合传播样式-标准色版.png