臨近放假,手頭的事情沒那麼多,老是摸魚也不好,還是寫寫部落格吧。
今天來聊聊:如何設計一個通用的查詢介面。
從一個場景開始
首先,我們從一個簡單的場景開始。現在,我需要一個訂單列表,用來查詢【我的訂單】,支援分頁,且支援高階搜尋。
整個查詢流程
我們先來設計下整個查詢的流程,我認為大致如下圖。簡單來說就是:接收查詢條件 -》 校驗條件 -》新增條件 -》 執行查詢 -》 轉換 VO -》 返回結果。
注意,因為不同公司用的語言或者程式碼分層可能不一樣,所以,我們沒必要糾結具體的程式碼實現,只要關注一些更高抽象層級的共性就行了。
一些疑問
看到上圖的流程,有的人可能會問一些問題,這裡我簡單回答下:
- 為什麼後端還要設定條件?前端不都設定好了嗎?
就拿【我的訂單】來說,查詢條件中肯定要有【訂單所屬人】這個條件吧,你放心把這個欄位交給前端來設定嗎?如果你選擇這麼做,那麼不好意思,這篇文章可能在浪費了您的時間。
- 為什麼不建議聯表構建 VO?
如果 VO 裡的資料都來自同一個 DB,按理來說,我們可以使用聯表的方法直接對映 VO,而不需要在程式碼中將實體轉 VO,像 mybatis 這種類庫就可以很輕易地做到這一點。但是,我不建議這麼做。因為以後你的資料來源可能會分庫分表,甚至改成第三方介面、ES、redis 等,到時你還能聯表嗎?當然,我只是建議儘量不要。
- 為什麼轉 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