武装你的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查询所有搞定,梭哈!不要问就是梭!

参考资料

相关文章
相关标签/搜索