武裝你的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的詳細介紹,可以看這里

參考資料


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM