武装你的WEBAPI-OData便捷查询

本文属于OData系列javascript

目录(可能会有后续修改)html


Introduction

前文大概介绍了下OData,本文介绍下它强大便捷的查询。(后面的介绍都基于最新的OData V4)前端

假设如今有这么两个实体类,并按照前文创建了OData配置。java

public class DeviceInfo
{
    [Key]
    [MaxLength(200)]
    public string DeviceId { get; set; }
    //....//
    public float Longitude { get; set; }
    public Config[] Layout { get; set; }
}

public class AlarmInfo
{
    [Key]
    [MaxLength(200)]
    public string Id { get; set; }
    public string DeviceId { get; set; }
    public string Type { get; set; }
    [ForeignKey("DeviceId")]
    public virtual DeviceInfo DeviceInfo { get; set; }
    public bool Handled { get; set; }
    public long Timestamp { get; set; }
}

Controller设置以下,很简单,核心就几行:git

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

[ODataRoute("({key})")]
[EnableQuery]
[ProducesResponseType(typeof(ODataValue<IDeviceInfo>), Status200OK)]
public IActionResult GetSingle([FromODataUri] string key)
{
    var result = _context.DeviceInfoes.Find(key);
    if (result == null) return NotFound(new ODataError() { ErrorCode = "404", Message = "Cannnot find key." });
    return Ok(result);
}

集合与单对象

使用OData,能够很简单的访问集合与单个对象。express

//访问集合
GET http://localhost:9000/api/DeviceInfoes

//访问单个对象,注意string类型须要用单引号。
GET http://localhost:9000/api/DeviceInfoes('device123')

$Filter

请求集合不少,咱们须要使用到查询来筛选数据,注意,这个查询是在服务器端执行的。json

//查询特定设备在一个时间的数据,注意这里须要手动处理一下这个ID为deviceid。
GET http://localhost:9000/api/AlarmInfoes('device123')?$filter=(TimeStamp ge 12421552) and (TimeStamp le 31562346785561)

$Filter支持不少种语法,可让数据筛选按照调用方的意图进行自由组合,极大地提高了API的灵活性。c#

$Expand

在查询alarminfo的时候,我很须要API可以返回全部信息,其中包括对应的deviceinfo信息。可是标准的返回是这样的:windows

{
    "@odata.context": "http://localhost:9000/api/$metadata#AlarmInfoes",
    "value": [
        {
            "id": "235314",
            "deviceId": "123",
            "type": "低温",
            "handled": true,
            "timestamp": 1589235890047
        },
        {
            "id": "6d2d3af3-2cf7-447e-822f-aab4634b3a13",
            "deviceId": "5s3",
            "type": null,
            "handled": true,
            "timestamp": 0
        }]
}

只有一个干巴巴的deviceid,要实现展现全部信息的功能,就只能再从deviceinfo的API请求一次,获取对应ID的devceinfo信息。api

这就是所谓的N+1问题:请求具备子集的列表时,须要请求1次集合+请求N个集合内数据的具体内容。会形成查询效率的下降。

不爽是否是?看下OData怎么作。请求以下:

GET http://localhost:9000/api/alarminfoes?$expand=deviceinfo

经过指定expand参数,就能够在返回中追加导航属性(navigation property)的信息。

本例中是使用的外键实现的。

{
"@odata.context": "http://localhost:9000/api/$metadata#AlarmInfoes(deviceInfo())",
"value": [
    {
        "id": "235314",
        "deviceId": "123",
        "type": "低温",
        "handled": true,
        "timestamp": 1589235890047,
        "deviceInfo": {
            "deviceId": "123",
            "name": null,
            "deviceType": null,
            "location": "plex",
            "description": "99",
            "longitude": 99,
            "layout": []
        }
    }

层级查询

实现了展开以后,那可否查询在多个层级内的数据呢?也能够,考虑查询全部longitude大于90的alarmtype。

GET http://localhost:9000/api/alarminfoes?$expand=deviceinfo&$filter=deviceinfo/longitude gt 90

注意,这里表示层级不是使用.而是使用的/。若是使用.容易被浏览器误识别,须要特别配置一下。

结果以下:

{
"@odata.context": "http://localhost:9000/api/$metadata#AlarmInfoes(deviceInfo())",
"value": [
    {
        "id": "235314",
        "deviceId": "123",
        "type": "低温",
        "handled": true,
        "timestamp": 1589235890047,
        "deviceInfo": {
            "deviceId": "123",
            "name": null,
            "deviceType": null,
            "location": "plex",
            "description": "99",
            "longitude": 99,
            "layout": []
        }
    }

any/all

可使用any/all来执行对集合的判断。
这个参考:

GET http://services.odata.org/V4/TripPinService/People?$filter=Emails/any(e: endswith(e, 'contoso.com'))

对于个人例子,我使用

GET http://localhost:9000/api/DeviceInfoes?$filter=layout/any(e: e/description eq '0')

服务器会弹出500服务器错误,提示:

System.InvalidOperationException: The LINQ expression 'DbSet<DeviceInfo>
    .Where(d => d.Layout
        .Any(e => e.Description.Contains(__TypedProperty_0)))' could not be translated. Either rewrite the query in a form that can be translated, or switch to client evaluation explicitly by inserting a call to either AsEnumerable(), AsAsyncEnumerable(), ToList(), or ToListAsync(). See https://go.microsoft.com/fwlink/?linkid=2101038 for more information.

查看文档以后,原来是我数据的层级比较深了,不在顶级层级类型的查询,在EF Core3.0的版本以前是支持的,在3.0之后就不支持了,主要是怕服务器内存溢出。若是真的要支持,那么就使用AsEnumerable()去实现客户端查询。

datetime相关

有同窗已经注意到了,我这边使用的timestamp的格式是unix形式的时间戳,而没有使用到js经常使用的ISO8601格式(2004-05-03T17:30:08+08:00)。若是须要使用到这种时间怎么办?

OData提供了一个语法,使用(datetime'2016-10-01T00:00:00')这种形式来表示Datetime,这样就可以实现基于datetime的查询。

查询顺序

谓词会使用如下的顺序:$filter, $inlinecount(在V4中已经替换为$count), $orderby, $skiptoken, $skip, $top, $expand, $select, $format。
filter总时最优先的,随后就是count,所以查询返回的count老是符合要求的全部数据的计数。

范例学习

官方有一个查询的Service,能够参考学习和试用,给了不少POSTMAN的示例。

不过有一些略坑的是,有的内容好比Paging里面,加入$count的返回是错的。

前端指南

查询的方法这么复杂,若是手动去拼接query string仍是有点虚的,好在有不少库能够帮助咱们作这个。

官方提供了各类语言的Client和Server的库:https://www.odata.org/libraries/

前端比较经常使用的有o.js

const response = await o('http://my.url')
  .get('User')
  .query({$filter: `UserName eq 'foobar'`}); 

console.log(response); // If one -> the exact user, otherwise an array of users

这样能够免去拼请求字符串的烦恼,关于o.js的详细介绍,能够看这里

参考资料