武裝你的WEBAPI-OData分頁查詢

本文屬於OData系列html

目錄git


Introduction

分頁是數據請求避免不了的問題,數據不少的狀況下,經過GET請求一次性返回全部的數據,不光性能底下,並且很差展現。github

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

傳統實現

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

/// <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便可實現分頁功能。json

OData實現分頁

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

$count

count關鍵字能夠隨同查詢一塊兒使用,使用$count=true的形式便可在查詢結果中追加返回符合查詢條件的全部的記錄的數量。windows

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這種形式。api

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查詢所有搞定,梭哈!不要問就是梭!

參考資料

相關文章
相關標籤/搜索