武裝你的WEBAPI-OData分頁查詢


本文屬於OData系列

目錄


Introduction

分頁是數據請求避免不了的問題,數據很多的情況下,通過GET請求一次性返回所有的數據,不光性能底下,而且不好展示。

分頁的原理就是客戶端請求服務器,服務器返回的數據是有限的數據(限制於pageSize),同時返回一個數據的總量count,方便客戶端進行處理。也有另外一種實現,使用nextlink指示下一頁的位置。

傳統實現

傳統的實現,我比較喜歡LINQ的Skip和Take方法。

/// <summary>
/// 有參GET請求
/// </summary>
/// <returns></returns>
[HttpGet("page")]
[ProducesResponseType(typeof(ReturnData<Page<UserInfoModel>>), Status200OK)]
[ProducesResponseType(typeof(ReturnData<string>), Status404NotFound)]
public async Task<ActionResult> Get(string username, int pageNo, int pageSize)
{
    if (pageSize <= 0 || pageNo <= 0) return BadRequest(new ReturnData<string>("Error request"));
    IEnumerable<UserInfoModel> result;
    if (string.IsNullOrWhiteSpace(username))
        result = _userManager.Users.Select(w => ToUserInfoModel(w)).ToList();
    else
        result = _userManager.Users.Select(w => ToUserInfoModel(w)).ToList().Where(w => w.Username.Contains(username));
    var response = result.Skip((pageNo - 1) * pageSize).Take(pageSize);
    Page<UserInfoModel> page = new Page<UserInfoModel>() { PageNo = pageNo, PageSize = pageSize, Result = response, TotalCount = result.Count() };
    return Ok(new ReturnData<Page<UserInfoModel>>(page));
}

通過傳遞username、pageNo和pageSize即可實現分頁功能。

OData實現分頁

OData查詢不需要后端再自行設計接受參數、實現等內容,並且支持兩種方式實現分頁:客戶端模式和服務器模式。首先我們需要補補幾個關鍵字的用法:(適用於OData V4)

$count

count關鍵字可以隨同查詢一起使用,使用$count=true的形式即可在查詢結果中追加返回符合查詢條件的所有的記錄的數量。

GET http://localhost:9000/api/devicedatas('ZW000001')?$count=true

注意這里不是返回的當前結果的計數。

{
    "@odata.context": "http://localhost:9000/api/$metadata#DeviceDatas",
    "@odata.count": 80,
    "value": [
        {
            "id": "554b1ed8-6429-4ad3-83f9-45c7696547e6",
            "deviceId": "ZW000001",
            "timestamp": 1589544960000,
            "dataArray": []
        },
        ...

$skip

skip關鍵字可以指定跳過的記錄數量,使用$skip=10這種形式。

GET http://localhost:9000/api/devicedatas('ZW000001')?$skip=30

返回的結果是跳過了前面的N條記錄。

$top

top關鍵字指定截取的符合查詢條件中的前n條記錄,使用top=10這種形式。

GET http://localhost:9000/api/devicedatas('ZW000001')?$top=10

$skiptoken

skiptoken這個東西和前面的東西都不一樣。skiptoken必須要服務器返回,一般來說是服務器根據主鍵的形式返回結果,然后調用方直接調用。經常出現在nextlink中,用於服務器分頁。

GET http://localhost:9000/api/devicedatas('ZW000001')?$skiptoken='554b1ed8-6429-4ad3-83f9-45c7696547e6'

注意這里不是返回的當前結果的計數。

{
    "@odata.context": "http://localhost:9000/api/$metadata#DeviceDatas",
    "value": [
        {
            "id": "554b1ed8-6429-4ad3-83f9-45c7696547e6",
            "deviceId": "ZW000001",
            "timestamp": 1589544960000,
            "dataArray": []
        },
        ...

客戶端模式

客戶端模式是客戶端主導的分頁實現,分頁的頁數數量之類的,都需要由客戶端指定,對客戶端來說,比較靈活。主要使用到count、skip和top三個關鍵字。

  1. 默認情況,服務器返回所有的記錄。
  2. 假設按照每頁10條記錄進行分頁,那么我們首次請求(請求第一頁)應該使用$count=true&$skip=0&$top=10獲取第一頁數據,同時帶有數據計數。
  3. 根據第一次請求獲得數據計數,可以快速計算總共的分頁數量。比如返回count=72,那么總共的頁數應該是72/10 + 1 =8頁(最后一頁只有2個數據)
  4. 生成每個頁碼的鏈接,第二頁應該是$count=true&$skip=10&$top=10
GET http://localhost:9000/api/devicedatas('ZW000001')?$count=true&$skip=10&$top=10
  • 這幾條命令需要先啟用,可以在startup.cs中修改:
app.UseMvc(
    routeBuilder =>
    {
        // the following will not work as expected
        // BUG: https://github.com/OData/WebApi/issues/1837
        // routeBuilder.SetDefaultODataOptions( new ODataOptions() { UrlKeyDelimiter = Parentheses } );
        routeBuilder.ServiceProvider.GetRequiredService<ODataOptions>().UrlKeyDelimiter = Parentheses;

        // global odata query options
        //routeBuilder.EnableDependencyInjection();
        routeBuilder.Select().Expand().Filter().OrderBy().MaxTop(600).Count().SkipToken();

        routeBuilder.MapVersionedODataRoutes("odata", "api", modelBuilder.GetEdmModels());
    });

服務端模式

客戶端模式靈活,但是有一個問題不好處理:客戶端在兩次請求的過程中,數據發生了變化,那會遇到一些意想不到的問題,比如說數據刪除了其中的一些,那么某條數據很有可能會同時出現在兩個頁。因此,可以讓服務器幫我們做分頁,服務器管理所有的數據,對兩次請求的數據變化也能及時感知,不會出現這個問題。

服務端模式需要使用到skiptoken和pagesize設置。

服務端模式,客戶端請求集合,服務器返回部分數據,同時提供一個nextlink,客戶端直接請求這個鏈接,就可以獲得更多的數據。

skiptoken啟用可以參考上面客戶端模式的代碼。pagesize是服務器最多每頁返回多少條數據的設置,可以在上面全局指定,也可以在具體的方法上面指定。

[ODataRoute]
[EnableQuery(PageSize = 1)]
[ProducesResponseType(typeof(ODataValue<IEnumerable<DeviceInfo>>), Status200OK)]
public IActionResult Get()
{
    return Ok(_context.DeviceInfoes.AsQueryable());
}

試着使用原始的方式進行請求。

GET http://localhost:9000/api/DeviceInfoes?$count=true

返回結果如下,能看到,返回的數據的結尾,多了一個@odata.nextLink,這個直接點擊,就可以直接請求下一組數據。在下一組數據中又會有在下一組數據的地址,直到最后一組數據。

{
    "@odata.context": "http://localhost:9000/api/$metadata#DeviceInfoes",
    "@odata.count": 3,
    "value": [
        {
            "deviceId": "ZW000001",
            "name": null,
            "deviceType": null,
            "imagePath": null,
            "layout": []
        }
    ],
    "@odata.nextLink": "http://localhost:9000/api/DeviceInfoes?$count=true&$skiptoken=deviceId-'ZW000001'"
}

注意:

  • 我這里主鍵使用的是字符串類型,並且用的是EF CORE 3.0,直接請求會返回服務器錯誤,需要自行指定string的比較模式,可以使用AsEnumerable()在System.Linq中處理。如果使用的主鍵是數值型,那么應該不會有這個問題。參考這里
  • 可以在請求中同時應用skip等客戶端模式的語法,構造自己需要的數據。

看完服務器模式,感覺這模式有點僵硬啊,只能一條一條地獲取下一個鏈接,我要直接跳幾頁的時候怎么辦呢?

首先你需要了解分頁的模式,我們請求http://services.odata.org/V4/TripPinService/People返回的nextlink會是這樣子的:

"@odata.nextLink": "https://services.odata.org/V4/TripPinService/People?%24skiptoken=8"

我這里使用到了官方提供的一個地址,返回了8條數據,同時指示了下一個鏈接的位置,很明顯,這個skiptoken=8是從第9個開始的,因此指定的只是一個開頭的地址,我們可以自行修改成其他數字。(前面說到skiptoken必須要服務生成,指的是后面的查詢模式需要是由服務器生成。)

那么對於第三頁就是skiptoken=16。但是由於服務器指定了分頁的大小8,我們查詢還是不方便,可以通過繼承EnableQueryAttribute實現,將這個[MyEnableQueryAttribute]替代剛剛的[EnableQuery]搬運

public class MyEnableQueryAttribute : EnableQueryAttribute
{
    public override IQueryable ApplyQuery(IQueryable queryable, ODataQueryOptions queryOptions)
    {
        int pagesize = xxx;
        var result = queryOptions.ApplyTo(queryable, new ODataQuerySettings { PageSize = pagesize }); 
        return result;
    }
} 

總結

OData使用客戶端模式的分頁和服務端的分頁都能夠很方便地實現分頁查詢。一個GET查詢全部搞定,梭哈!不要問就是梭!

參考資料


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM