zoukankan      html  css  js  c++  java
  • 提供一种业务系统非核心信息不连表查询解决方案

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

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

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

  • 相关阅读:
    asp.net core 发布centos 7 遇到的坑
    模拟EF CodeFist 实现自己的ORM
    EF+Redis(StackExchange.Redis)实现分布式锁,自测可行
    Sqlite 梳理
    mina.net 梳理
    C# 读取Execl和Access数据库
    MVC4.0网站发布和部署到IIS7.0上的方法
    看懂SqlServer查询计划
    C#数据表加锁解锁
    『C#基础』数据库死锁笔记
  • 原文地址:https://www.cnblogs.com/tuofan/p/11378856.html
Copyright © 2011-2022 走看看