zoukankan      html  css  js  c++  java
  • 武装你的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的详细介绍,可以看这里

    参考资料

  • 相关阅读:
    剑指 Offer——13. 调整数组顺序使奇数位于偶数前面
    剑指 Offer——3. 从尾到头打印链表
    剑指 Offer——2. 替换空格
    剑指 Offer——1. 二维数组中的查找
    LeetCode 905. Sort Array By Parity 按奇偶校验排列数组
    LeetCode 448. Find All Numbers Disappeared in an Array找到所有数组中消失的元素
    SSH 代码笔记
    anaconda3安装caffe
    opencv多版本安装
    人脸文章与数据库
  • 原文地址:https://www.cnblogs.com/podolski/p/12880185.html
Copyright © 2011-2022 走看看