如何設計一個更通用的查詢介面

子月生發表於2022-01-19

臨近放假,手頭的事情沒那麼多,老是摸魚也不好,還是寫寫部落格吧。

今天來聊聊:如何設計一個通用的查詢介面

從一個場景開始

首先,我們從一個簡單的場景開始。現在,我需要一個訂單列表,用來查詢【我的訂單】,支援分頁,且支援高階搜尋。

整個查詢流程

我們先來設計下整個查詢的流程,我認為大致如下圖。簡單來說就是:接收查詢條件 -》 校驗條件 -》新增條件 -》 執行查詢 -》 轉換 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

相關文章