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

Posted tuofan

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了提供一种业务系统非核心信息不连表查询解决方案相关的知识,希望对你有一定的参考价值。

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

本文针对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 ** 省略业务逻辑代码 **;

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

以上是关于提供一种业务系统非核心信息不连表查询解决方案的主要内容,如果未能解决你的问题,请参考以下文章

分布式事务之最大努力通知型

ERP

Spring容器装饰者模式应用之实现业务类与服务类自由组合的解决方式

连表查询都用Left Join吧

JavaGraphQL提供数据接口新思路之数据聚合解决方案

LRU算法的实现