• 武装你的WEBAPI-OData便捷查询


    本文属于OData系列

    目录


    Introduction

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

    假设现在有这么两个实体类,并按照前文建立了OData配置。

    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设置如下,很简单,核心就几行:

    [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,可以很简单的访问集合与单个对象。

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

    $Filter

    请求集合很多,我们需要使用到查询来筛选数据,注意,这个查询是在服务器端执行的。

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

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

    $Expand

    在查询alarminfo的时候,我很需要API能够返回所有信息,其中包括对应的deviceinfo信息。但是标准的返回是这样的:

    {
        "@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信息。

    这就是所谓的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的详细介绍,可以看这里

    参考资料

  • 相关阅读:
    Apache ECharts
    navicate10破解版 in win
    mysql5.7.23免安装配置说明in win7
    ubuntu安装intellij IDEA ultimate破解
    java1015 leetcode1 twosum approach2 Map-doc generic PESC type argument/(? extends | super %bounded) parameterized type
    笔试题学习(dp,重叠子问题,卡特兰数,手电过桥,最长公共子序列)
    selfish mining:block‐withholding attacks
    矿工找到block的概率分布函数和函数图像
    proof of reserves and proof of liabilities and fractional reserve
    soft fork and hard fork
  • 原文地址:https://www.cnblogs.com/podolski/p/12880185.html
Copyright © 2020-2023  润新知