• 如何设计一个更通用的查询接口


    临近放假,手头的事情没那么多,老是摸鱼也不好,还是写写博客吧。

    今天来聊聊:如何设计一个通用的查询接口

    从一个场景开始

    首先,我们从一个简单的场景开始。现在,我需要一个订单列表,用来查询【我的订单】,支持分页,且支持高级搜索。

    整个查询流程

    我们先来设计下整个查询的流程,我认为大致如下图。简单来说就是:接收查询条件 -》 校验条件 -》添加条件 -》 执行查询 -》 转换 VO -》 返回结果

    注意,因为不同公司用的语言或者代码分层可能不一样,所以,我们没必要纠结具体的代码实现,只要关注一些更高抽象层级的共性就行了。

    interface_design_006.png

    一些疑问

    看到上图的流程,有的人可能会问一些问题,这里我简单回答下:

    1. 为什么后端还要设置条件?前端不都设置好了吗?

    就拿【我的订单】来说,查询条件中肯定要有【订单所属人】这个条件吧,你放心把这个字段交给前端来设置吗?如果你选择这么做,那么不好意思,这篇文章可能在浪费了您的时间。

    1. 为什么不建议联表构建 VO?

    如果 VO 里的数据都来自同一个 DB,按理来说,我们可以使用联表的方法直接映射 VO,而不需要在代码中将实体转 VO,像 mybatis 这种类库就可以很轻易地做到这一点。但是,我不建议这么做。因为以后你的数据源可能会分库分表,甚至改成第三方接口、ES、redis 等,到时你还能联表吗?当然,我只是建议尽量不要。

    1. 为什么转 VO,直接返回不行吗?

    我们的实体中的字段,有可能太多,也有可能太少。多指的是,我返回了一些不能返回的字段,例如用户密码;少指的是,前端要的字段,实体里不一定有。这时有人可能会问,如果实体里没有不能返回的字段,且能够完全满足前端的所有字段需求,是不是就可以直接返回。这个嘛,你真的能保证吗?

    具体代码实现

    这里提供一种简单的 java 实现。

    Controller

    @RestController
    @RequestMapping("/order")
    public class OrderController {
        @Resource
        private OrderService orderService;
        
        @PostMapping("/queryPage")
        public DataResponse<Page<Order2MyListVO>> queryPage(@RequestBody OrderQuery query) {
            return DataResponse.of(orderService.queryPage(query));
        }
    }
    

    Service

    @Service
    public class OrderService {
        @Resource
        private OrderGateway orderGateway;
        
        public Page<Order2MyListVO> queryPage(OrderQuery query) {
            // 校验
            validate(query);
            
            // 添加条件
            addCon(query);
            
            // 执行查询
            Page<OrderE> sourcePage = orderGateway.queryPage(query);
            
            // 转换为VO并返回结果
            return ConvertUtils.convertPage(sourcePage, OrderConverter::convert2MyListVO);
        }
    }
    

    查询场景变多了

    好了,说完单个场景,我们再来说说多个场景的情况。我需要增加【商场的订单】、【下属的订单】等等。

    加接口or不加?

    这时,我们有两种选择:加接口 or 不加接口?如果加接口的话,随着场景的增加,我们的接口会越来越多。我相信更多的人会选择不加接口,即用一个查询接口来搞定所有场景。

    如何区分不同场景?

    那么问题来了,不加接口的情况下,我们应该怎么设计呢?

    我们会发现,不同的场景,查询的流程都是一样的,只是在校验条件、添加条件、转换 VO 三个节点的逻辑上有所区别。对应上图的步骤 2、3、8。于是,针对这三个节点,我们需要根据不同的场景走不同的逻辑,类似于大家常说的策略模式,当然,这样做要有一个前提,就是我们能够区分请求是来自哪个场景。

    其中一个实现就是,在 query 中增加一个 scenarioFlag 字段,由调用方传值,当查【我的订单】时值为 OrderQryPage2Me,当查【商场的订单】时值为 OrderQryPage2Market······

    如何实现?

    这里我还是提供简单的 java 实现。实际使用的话会更复杂一些。

    Controller

    这时,返回值的泛型就不能写死了,因为同一个接口有可能返回不同的类型。这一点相信很多人都没法接受。

    @RestController
    @RequestMapping("/order")
    public class OrderController {
        @Resource
        private OrderService orderService;
        
        @PostMapping("/queryPage")
        public DataResponse<?> queryPage(@RequestBody OrderQuery query) {
            return DataResponse.of(orderService.queryPage(query));
        }
    }
    

    Service

    我用的是阿里的 Cola 框架来处理不同场景的策略分发,每个场景中差异化的逻辑都放在一个可插拔的的扩展点里,而扩展点根据【业务-用例-场景】来划分。具体实现如下。

    前面说过,不同场景只是在校验条件、添加条件、转换 VO 三个节点的逻辑上有所区别,然而,还是存在某些场景,连执行查询这个节点的逻辑也不一样。这里也兼容了这种情况。

    @Service
    public class OrderService {
        @Resource
        private ExtensionExecutor extensionExecutor;
        @Resource
        private OrderGateway orderGateway;
        
        public Object queryPage(OrderQuery query) {
            // 设置场景
            BizScenario bizScenario = BizScenario.valueOf(
                ORDER, // 订单业务
                ORDER_QUERY, // 订单查询 
                query.getScenarioFlag() // 具体场景
            );
            
            // 根据不同的场景走不同的逻辑:校验、加条件、转VO
            // 这里的转VO逻辑还没走,只是把逻辑作为Function设置到query里面
            Object result = extensionExecutor.execute(
                    OrderQryExtPt.class, 
                    bizScenario, 
                    x -> x.extendQuery(query)
                    );
            // 如果返回非空对象,则直接将结果返回,不再走通用查询
            if(result != null) {
                return result;
            }
            
            // 执行通用查询
            result = orderGateway.queryPage(query);
            
            // 这里才开始走转VO的逻辑
            if(query.getConvertMethod() != null) {
                return query.getConvertMethod().apply(result);
            }
            return result;
        }
    }
    

    具体的扩展点如下。里面一般就是差异化的三个节点逻辑。

    @Extension(
            bizId = ORDER, // 订单业务
            useCase = ORDER_QUERY, // 订单查询 
            scenario = OrderQryPage2Me // 我的订单
            )
    public class OrderQryPage2MeExt implements OrderQryExtPt {
    
        @Override
        public Object extendQuery(OrderQuery query) {
            
            // 校验
            validate(query);
            
            // 添加条件 zzs001
            addCon(query);
            
            // 设置转换VO的逻辑
            Function<Object, Page<Order2MyListVO>> convertMethod = x -> {
                
                Page<OrderE> sourcePage = (Page<OrderE>)x;
                
                return ConvertUtils.convertPage(sourcePage, OrderConverter::convert2MyListVO);
            };
            query.setConvertMethod(convertMethod);
            
            return null;
        }
    
    }
    

    要不要万能VO?

    上面的例子中,针对不同的场景,我会提供不同的 VO。但有些人会尝试用一个万能的 VO 来应对所有的场景,我认为,这是非常不利于维护的做法。随着场景的增加,你的 VO 字段会越来越多,你根本区分不出来哪些场景需要哪些字段,最重要的是,这种通用 VO 让很多场景不得不去查询一些不需要的字段,而耗费性能。

    结语

    以上就是我对查询接口设计的一些想法,虽然不算成熟,但也不是纸上谈兵,因为我们的订单系统现在采用的就是这种方式,目前落地效果还是可以的。当然,可能是因为业务还没那么复杂吧。

    最后,感谢阅读,欢迎交流、指正。

    本文为原创文章,转载请附上原文出处链接:https://www.cnblogs.com/ZhangZiSheng001/p/15822105.html

  • 相关阅读:
    MongDB in Rails 3 using mongo_mapper
    Cucumber + Capybara What we need for rails integration test
    HTML5 drag & drop 拖拽与拖放简介
    Installing GitLab 2.1 on Centos 6
    CakePHP的belongsTo关系中关于外键关联字段都不是id字段的问题
    Mod_rewrite in Cakephp using Apache
    How to Configure Static IP Address on CentOS 6.3 Linux Server
    mongoDB 入门指南、示例
    MVC3/4项目开发中遇到的ajax提交Json数据到后台Controller处理(接收参数)
    ASP.NET jQuery (表单中使用回车在TextBox之间向下移动)
  • 原文地址:https://www.cnblogs.com/ZhangZiSheng001/p/15822105.html
Copyright © 2020-2023  润新知