提供一种业务系统非核心信息不连表查询解决方案

一种业务系统非核心信息不连表查询解决方案

本文针对java开发且采用前后端分离的开发模式,非java开发可能作用不大。同时数据库以mysql为例,部分表述只做示例,并非严谨的mysql语句。

普通的业务系统开发过程中,下面描述的这种需求应该是比较常见的。一个申请单,需要显示申请人名字,审核人名字。

这里涉及到两张表:申请单(t_apply), 用户(t_user),后台数据表我们可能会这么设计:

// 方案一
t_apply(
    apply_id,
    apply_no,
    ***
    apply_user_id,
    apply_user_name,
    apply_auditor_id,
    apply_auditor_name
    ***
)
t_user(
    user_id,
    user_name
)

也可能这么设计

// 方案二
t_apply(
    apply_id,
    apply_no,
    ***
    apply_user_id,
    apply_auditor_id
    ***
)
t_user(
    user_id,
    user_name
)

方案一冗余了申请人名字和审核人名字字段,很好,在查询的时候不需要连表查询。但要考虑,如果这个用户改名了呢,申请单的名字要不要做修改?如果不需要,保存并在后期显示当时的快照即可,那么本文可以跳过了。如果需要,那么冗余就显得没有必要了。因为这里更新的成本有些大。

方案二存的是ID,没有冗余的字段,很好,只是查询的时候要连表查。稍显麻烦。

连表对于数据库,是一个比较大的性能开销。多数企业做应用架构并未考虑读写分离,连表过多更会产生影响,阿里的开发规范也有提到连表查询最好不要超过3张表,尤其是这种非核心但又非得要的信息,连表查询更加显得不重要且耗数据库性能。

1、当然这个问题也有其他的解决方案

方案W:把问题丢给前端。后台做基础数据查询,申请单、用户都返回,前端根据用户ID,去找用户名字,然后显示。这种方式,估计前端看了想打人。

方案S:把数据库的性能转移给java。不连表查询,任何查询都不连表。举个例子,对于申请单列表:

  1. 查到所有申请单,一个列表:List applies。
  2. 遍历 applies 得到一个用户ID列表List userIds。
  3. 根据 userIds 查到List users
  4. 遍历 applies ,根据 applyUserId , 遍历 users ,找到 applyUserName。这一步也可事先将users转为键值为id的map,然后更快定位数据。

如此,将数据库的压力转移给了java。这点查询与循环,对于java来说,还是没啥问题的。然而,此刻java开发人员就没有那么高兴了。不让连表查询,每次这么查询遍历,够累的。

2、不想重复劳动,那就再想想办法

这个填塞的过程其实是很常见的,比如再来一个公司,申请单有一个归属公司 company_id , 公司名字存在基础表 t_company。申请单要显示公司名字,java开发人员在用方案S进行数据查询的时候,公司和用户的操作,步骤完全一致,只是对应的实体不同。

因为懒,不想写重复的代码,哪怕是重复逻辑的代码,所以就得想想办法

好,来说说本人想到的方案:那就是抽离这一部分填塞的业务,用一个横切来实现。把方案W和方案S结合一下。基础数据(如用户)提供基本查询方法,业务开发(如申请单)调用基本查询方法,实现数据的填塞。

3、下面讲讲具体怎么做

2个注解,一个加在视图对象字段上,表达该字段需要从别的表中查。一个加在控制层方法上,表达该方法需要处理返回值字段连表查询。

字段层注解

@Target({ElementType.FIELD})
@Retention(RetentionPolicy.RUNTIME)
public @interface Converted {

    @ApiParam("依赖字段,根据当前类实体存储的字段,得到目标字段")
    String dependProperty();

    @ApiParam("BeanService,如 UserService.class")
    Class<? extends Object> bean();

    @ApiParam("形式为:List<T> refMethod(List<dependProperty>) , 或者Map<String,String> refMethod(List<dependProperty>)的关联实体方法")
    String refMethod() default "listByIds";

    @ApiParam("关联实体组成Map的key值")
    String refKey() default "id";

    @ApiParam("如果method返回值为List<T>使用T.getLabel()如T.getName() 给当前关联实体赋值")
    String refLabel() default "name";
    
    @ApiParam("关联实体组成Map的key值类型")
    Class<? extends Object> refKeyClass();

    @ApiParam("未能成功转换给的默认值")
    String defaultValue() default "";

    
}

以刚刚的申请单为例,解释下各个标注含义。

  1. dependProperty即为apply.applyUserId,其存在于申请表,select * from t_apply单表查询就能得到。
  2. bean即为用户服务,需要是能取到的spring的bean。其提供查询方法,能查询到用户信息。
  3. refMethod即为用户提供的能查到用户信息的方法名。这个方法名参数类型必须是List,List的泛型类型必须是dependProperty的类型。
  4. refKey即为user.userId。即apply.applyUserId 对应到user的字段名。
  5. refLabel即为user.userName。及目标字段,最后要显示出来的字段内容。
  6. refKeyClass,即为dependProperty的类型。这里实际上可以通过反射取到,后面也可能将其优化掉。
    7.defaultValue的意思是:没取到咋办,设个默认值

方法层注解

@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface FieldConversion {

}

没啥好说的,做个控制,不是所有的方法都需要连表查,加上才来查。

好,接下来看下切片处理咋写,思路跟方案S差不多。这部分内容太长,我展示下关键代码,主要是用反射取值,设置,加一些泛型处理。

/**
 * Created by tuofan 
 */
@SuppressWarnings("unchecked")
public class FieldConvertUtils {

    private static Logger logger = LoggerFactory.getLogger(FieldConvertUtils.class);

    private FieldConvertUtils() {
    }

    /**
     * @param list
     */
    public static <T> void convertList(List list) throws NoSuchFieldException, NoSuchMethodException {
        if (CollectionUtils.isEmpty(list)) {
            return;
        }
        if (list.get(0) == null) {
            return;
        }
        Field[] fields = FieldConvertUtils.getFields(list.get(0));
        if (fields == null || fields.length == 0) {
            return;
        }
        Map<Field, Map<T, String>> refBeanValueMap = FieldConvertUtils.getFieldFeignValueMap(list, fields);
        FieldConvertUtils.convertValue(list, fields, refBeanValueMap);
    }


    private static Field[] getFields(Object object) {
        return object.getClass().getDeclaredFields();
    }

    /**
     * 获取列表中要转换所有key和value
     *
     * @param list
     * @param fields
     * @return
     */
    private static <T> Map<Field, Map<T, String>> getFieldFeignValueMap(List list, Field[] fields) throws NoSuchFieldException, NoSuchMethodException {
        // 存放每个字段,转换前和转换后的对应值
        Map<Field, Map<T, String>> refFiledValueMap = Maps.newHashMap();
        for (Field field : fields) {
            Converted converted = field.getAnnotation(Converted.class);
            if (converted == null) {
                continue;
            }
            List<T> keyList = extractList(list, field);
            Map<T, String> keys2ValuesMap = convertKeys2Values(keyList, field);
            refFiledValueMap.put(field, keys2ValuesMap);
        }
        return refFiledValueMap;
    }

    private static <T> Map<T, String> convertKeys2Values(List<T> keys, Field field) throws NoSuchMethodException, NoSuchFieldException {
        if (CollectionUtils.isEmpty(keys)) {
            return Maps.newHashMap();
        }
        Converted converted = field.getAnnotation(Converted.class);
        Object beanService = SpringUtils.getBean(converted.bean());
        Object[] args = {keys};
        Method refMethod = getMethod(beanService.getClass(), converted.refMethod());
        Class returnClazz = refMethod.getReturnType();
        // 返回值是map
        if (returnClazz.isAssignableFrom(Map.class)) {
            return (Map<T, String>) ReflectionUtils.invokeMethod(refMethod, beanService, args);
        }
        // 返回值是list
        if (returnClazz.isAssignableFrom(Collection.class)) {
            Collection collection = (Collection) ReflectionUtils.invokeMethod(refMethod, beanService, args);
            if (CollectionUtils.isEmpty(collection)) {
                return Maps.newHashMap();
            }
            return convertList2MapFilterNull(collection, converted.refKey(),
                    converted.refLabel());
        }
        logger.error("返回值类型={}暂不支持转换,目前仅支持 Collection 和 Map ", returnClazz.getName());
        return Maps.newHashMap();
    }


    private static <T> Map<T, String> convertList2MapFilterNull(Collection<?> collection, String keyProperty, String valueProperty) throws NoSuchFieldException {
        Map<T, String> map = Maps.newHashMap();
        for (Object ele : collection) {
            Field fKey = ele.getClass().getDeclaredField(keyProperty);
            T key = extractTValue(ele, fKey);
            Field fValue = ele.getClass().getDeclaredField(valueProperty);
            ReflectionUtils.makeAccessible(fValue);
            String value = (String) ReflectionUtils.getField(fValue, ele);
            map.put(key, value);
        }
        return map;
    }


    /**
     * 作为beanService 的参数
     */
    private static <T> List<T> extractList(List list, Field field) throws NoSuchFieldException {
        List<T> keyList = Lists.newArrayList();
        Converted converted = field.getAnnotation(Converted.class);
        for (Object returnValue : list) {
            // 获取要转换的key值
            T key = extractKey(returnValue, converted);
            if (key != null) {
                keyList.add(key);
            }
        }
        return keyList;
    }

    /**
     * 获取key,注解上有dependProperty属性,则取这个属性的值,否则就是当前filed的值
     *
     * @param returnValue
     * @param converted
     * @return
     */
    private static <T> T extractKey(Object returnValue, Converted converted) throws NoSuchFieldException {
        Field field = returnValue.getClass().getDeclaredField(converted.dependProperty());
        return extractTValue(returnValue, field);
    }

    private static <T> T extractTValue(Object returnValue, Field field) throws NoSuchFieldException {
        ReflectionUtils.makeAccessible(field);
        Object obj = ReflectionUtils.getField(field, returnValue);
        if (obj == null) {
            return null;
        }

        return (T) obj;
    }

    /**
     * 根据取到的值进行转换
     *
     * @param list
     * @param refFiledValueMap
     */
    private static <T> void convertValue(List list, Field[] fields, Map<Field, Map<T, String>> refFiledValueMap) throws NoSuchFieldException {
        for (Object returnValue : list) {
            for (Field field : fields) {
                Converted converted = field.getAnnotation(Converted.class);
                if (converted == null) {
                    continue;
                }
                T key = extractKey(returnValue, converted);
                ReflectionUtils.makeAccessible(field);
                if (refFiledValueMap.containsKey(field) && refFiledValueMap.get(field).containsKey(key)) {
                    ReflectionUtils.setField(field, returnValue, refFiledValueMap.get(field).get(key));
                } else {
                    ReflectionUtils.setField(field, returnValue, converted.defaultValue());
                }
            }
        }
    }

    /**
     * @param clazzT
     * @param methodName
     * @return
     */
    private static Method getMethod(Class clazzT, String methodName) {
        if (clazzT == null || clazzT == Object.class || StringUtils.isEmpty(methodName)) {
            return null;
        }
        for (; clazzT.getSuperclass() != Object.class; clazzT = clazzT.getSuperclass()) {
            Method[] methods = clazzT.getDeclaredMethods();
            for (Method m : methods) {
                if (m.getName().equals(methodName)) {
                    return m;
                }
            }
        }
        return null;
    }
}

4、最后看看怎么用

vo字段上加注解

@ApiModelProperty(value = "审核人ID")
private Long auditorId;

@Converted(dependProperty = "auditorId", refKeyClass = Integer.class, bean = MobileUserService.class, refLabel = "userName")
@ApiModelProperty(value = "审核人姓名")
private String auditorName;

controller上加标签

@PostMapping("listPage")
@FieldConversion
public ResultVO<IPage<ModelVO>> listPage(@RequestBody ModelQuery modelQuery) {
    return ** 省略业务逻辑代码 **;
}

总的来说,就是将公共的操作抽象出来,用切片的方式实现,让代码更加整洁。

posted @ 2019-08-19 18:37  小拓同学  阅读(427)  评论(1编辑  收藏  举报