- 使用 Spring Initializr 创建 Spring Boot 应用程序
- 在Spring Boot中配置Cassandra
- 在 Spring Boot 上配置 Tomcat 连接池
- 将Camel消息路由到嵌入WildFly的Artemis上
临近放假,手头的事情没那么多,老是摸鱼也不好,还是写写博客吧。
今天来聊聊:如何设计一个通用的查询接口。
首先,我们从一个简单的场景开始。现在,我需要一个订单列表,用来查询【我的订单】,支持分页,且支持高级搜索。
我们先来设计下整个查询的流程,我认为大致如下图。简单来说就是:接收查询条件 -》 校验条件 -》添加条件 -》 执行查询 -》 转换 VO -》 返回结果。
注意,因为不同公司用的语言或者代码分层可能不一样,所以,我们没必要纠结具体的代码实现,只要关注一些更高抽象层级的共性就行了。
看到上图的流程,有的人可能会问一些问题,这里我简单回答下:
就拿【我的订单】来说,查询条件中肯定要有【订单所属人】这个条件吧,你放心把这个字段交给前端来设置吗?如果你选择这么做,那么不好意思,这篇文章可能在浪费了您的时间。
如果 VO 里的数据都来自同一个 DB,按理来说,我们可以使用联表的方法直接映射 VO,而不需要在代码中将实体转 VO,像 mybatis 这种类库就可以很轻易地做到这一点。但是,我不建议这么做。因为以后你的数据源可能会分库分表,甚至改成第三方接口、ES、redis 等,到时你还能联表吗?当然,我只是建议尽量不要。
我们的实体中的字段,有可能太多,也有可能太少。多指的是,我返回了一些不能返回的字段,例如用户密码;少指的是,前端要的字段,实体里不一定有。这时有人可能会问,如果实体里没有不能返回的字段,且能够完全满足前端的所有字段需求,是不是就可以直接返回。这个嘛,你真的能保证吗?
这里提供一种简单的 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 不加接口?如果加接口的话,随着场景的增加,我们的接口会越来越多。我相信更多的人会选择不加接口,即用一个查询接口来搞定所有场景。
那么问题来了,不加接口的情况下,我们应该怎么设计呢?
我们会发现,不同的场景,查询的流程都是一样的,只是在校验条件、添加条件、转换 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 让很多场景不得不去查询一些不需要的字段,而耗费性能。
以上就是我对查询接口设计的一些想法,虽然不算成熟,但也不是纸上谈兵,因为我们的订单系统现在采用的就是这种方式,目前落地效果还是可以的。当然,可能是因为业务还没那么复杂吧。
最后,感谢阅读,欢迎交流、指正。
本文为原创文章,转载请附上原文出处链接:https://www.cnblogs.com/ZhangZiSheng001/p/15822105.html
分层,抽象,高内聚,低耦合
我正在尝试在我的代码库中为我正在编写的游戏服务器更多地使用接口(interface),并了解高级概念以及何时应该使用接口(interface)(我认为)。在我的例子中,我使用它们将我的包相互分离,并使
我有一个名为 Widget 的接口(interface),它在我的整个项目中都在使用。但是,它也用作名为 Widget 的组件的 Prop 。 处理此问题的最佳方法是什么?我应该更改我的 Widget
有一个接口(interface)可以是多个接口(interface)之一 interface a {x:string} interface b {y:string} interface c {z:st
我遇到了一种情况,我需要调用第三方服务来获取一些信息。这些服务对于不同的客户可能会有所不同。我的界面中有一个身份验证功能,如下所示。 interface IServiceProvider { bool
在我的例子中,“RequestHandlerProxy”是一个结构,其字段为接口(interface)“IAdapter”,接口(interface)有可能被调用的方法,该方法的输入为结构“Reque
我有一个接口(interface)Interface1,它已由类A实现,并且设置了一些私有(private)变量值,并且我将类A的对象发送到下一个接受输入作为Interface2的类。那么我怎样才能将
假设我有这样的类和接口(interface)结构: interface IService {} interface IEmailService : IService { Task SendAs
有人知道我在哪里可以找到 XML-RPC 接口(interface)的定义(在 OpenERP 7 中)?我想知道创建或获取对象需要哪些参数和对象属性。每个元素的 XML 示例也将非常有帮助。 最佳答
最近,我一直在阅读有关接口(interface)是抽象的错误概念的文章。一篇这样的帖子是http://blog.ploeh.dk/2010/12/02/InterfacesAreNotAbstract
如果我有一个由第三方实现的现有 IInterface 后代,并且我想添加辅助例程,Delphi 是否提供了任何简单的方法来实现此目的,而无需手动重定向每个接口(interface)方法?也就是说,给定
我正在尝试将 Article 数组分配给我的 Mongoose 文档,但 Typescript 似乎不喜欢这样,我不知道为什么它显示此警告/错误,表明它不可分配. 我的 Mongoose 模式和接口(
我有两个接口(interface): public interface IController { void doSomething(IEntity thing); } public inte
是否可以创建一个扩展 Serializable 接口(interface)的接口(interface)? 如果是,那么扩展接口(interface)的行为是否会像 Serilizable 接口(int
我试图在两个存储之间创建一个中间层,它从存储 A 中获取数据,将其转换为相应类型的存储 B,然后存储它。由于我需要转换大约 50-100 种类型,我希望使用 map[string]func 并根据 s
我正在处理一个要求,其中我收到一个 JSON 对象,其中包含一个日期值作为字符串。我的任务是将 Date 对象存储在数据库中。 这种东西: {"start_date": "2019-05-29", "
我们的方法的目标是为我们现有的 DAO 和模型类引入接口(interface)。模型类由各种类型的资源 ID 标识,资源 ID 不仅仅是随机数,还带有语义和行为。因此,我们必须用对象而不是原始类型来表
Collection 接口(interface)有多个方法。 List 接口(interface)扩展了 Collection 接口(interface)。它声明与 Collection 接口(int
我有一个 Java 服务器应用程序,它使用 Jackson 使用反射 API 对 DTO 进行一般序列化。例如对于这个 DTO 接口(interface): package com.acme.libr
如果我在 Kotlin 中有一个接口(interface): interface KotlinInterface { val id: String } 我可以这样实现: class MyCla
我知道Java中所有访问修饰符之间的区别。然而,有人问了我一个非常有趣的问题,我很难找到答案:Java 中的 private 接口(interface)和 public 接口(interface)有什
我是一名优秀的程序员,十分优秀!