首页 > 解决方案 > OData v4 按多个基数属性排序

问题描述

我有以下网址:

{{hostUrl}}/odata/TasksOData?$select=Id,Name,State,TaskRuns,LastChangedAt,LastChangedBy&$expand=TaskRuns($orderby=RunAt desc;$top=1;$select=Status,RunAt,RunBy)&$top=16&$orderby=TaskRuns/RunBy

我想从 TaskRuns(RunBy) 中按属性订购任务。TaskRuns 是一个我只想考虑第一项的集合。

我得到错误:"message": "The parent value for a property access of a property 'RunBy' is not a single value. Property access can only be applied to a single value.",

RunBy 是一个 GUID 字段。RunAt 出现同样的问题,它是 DateTime 字段。我测试了其他场景,似乎问题是因为 TaskRuns 是一个集合。即使花费的时间比预期的要长,以下网址也可以工作:

{{hostUrl}}/odata/TasksOData?$select=Id,Name,State,TaskRuns,LastChangedAt,LastChangedBy&$expand=TaskRuns($orderby=RunAt desc;$top=1;$select=Status),Script($select=Name)&$top=16&$orderby=Script/Name desc

后端:OData v4、asp 核心、v7.1.0

如何实现?谢谢!

标签: asp.net-coreodataodata-v4

解决方案


oData v4 规范不支持这种类型的嵌套排序。但这并不意味着您无法使用 oData 获得类似的结果集。

问题是我们想要对子集合内的一行中的 a 属性的顶级响应的结果进行排序。

这不受支持,您只能根据该集合中行的属性对每个集合进行排序。$orderby只能引用单例属性路径对结果进行排序。您可以指定结果中记录的排序顺序$expand,但 OOTB 不能使用集合中任意行的属性来指定顶级排序顺序。

您检索此类数据的选项分为 3 类:

  1. 在控制器上创建自定义端点以执行结果集

    • 这应该是最容易实现的,但只有在您控制 API 时才可行。
    • 上面的 Linq 查询非常简单,示例控制器函数可能如下所示:

    [HttpGet]
    public IHttpActionResult GetRecentTaskRuns()
    {
        var query = from task in _db.Tasks
                    let lastRun = task.TaskRuns.OrderByDescending(t => t.RunAt).FirstOrDefault()
                    select new
                    {
                        task.Id,
                        task.Name,
                        task.State,
                        // Consider not returning this as a collection at all
                        //TaskRuns = task.TaskRuns.OrderByDescending(t => t.RunAt).Take(1).ToArray(),
                        LastChangedAt,
                        LastChangedBy,
                        // Just return LastRun, instead of TaskRuns.
                        LastRun = lastRun
                    };
    
        return Json(query.OrderBy(t => t.LastRun.RunBy));
    }
    

    不要害怕在你的控制器上创建自定义端点来实现特定的业务需求,你的 API 的目的是方便数据访问,是的 OData 为调用者公开了美妙的语法来完全控制要下载的数据,但你可以帮助他们出去。

  2. 更改您的查询以从 TaskRuns 控制器中检索
    • API 端不需要进行任何更改
    • 这会改变结果集和数据的结构
    • 您可以使用按结果$apply返回,这类似于原始要求
    • 有关更多详细信息,请参见下文$apply
  3. 覆盖EnableQueryAttribute或以其他方式实现您自己的$orderby解释器
    • 这远远超出了本次讨论的范围,但是嘿,有可能,您可以实现自己的解释器$orderby以及如何将其应用于底层数据库查询,如果您有能力这样做,您应该考虑选项 1。 ..

如果您不想或无法修改 API 本身,只有上面的选项 2 会为您提供帮助。


让我们探讨一下原因,例如,如果每个Task都有多个TaskRunsTaskRuns应该使用其中哪一个来获取RunBy值?

您可能会考虑尝试 MIN 或 MAX 函数,但它们不会起作用,OData v4 规范中没有以下规定:

{{hostUrl}}/odata/TasksOData?$orderby=Max(TaskRuns/RunBy)
or
{{hostUrl}}/odata/TasksOData?$orderby=TaskRuns/Max(RunBy)

使用 $Apply

当您想根据子集合限制结果时,我们可以使用$apply函数,但是类似的规则类型适用于只能对根集合进行操作。所以切换你的查询,这样子集合就是根!

即使使用$apply我也无法获得与原始查询所暗示的完全相同的结构或顺序,但此查询显示了另一种选择

注意:如果您的日期字段不是,您将无法使聚合工作DateTimeOffset

{{hostUrl}}/odata/TaskRunsOData?  
$apply=groupby((Task/Id,Task/Name,Task/State,Task/LastChangedAt,Task/LastChangedBy), aggregate(RunAt with max as RunAt,RunBy with max as RunBy)&$top=16&$orderby=RunBy DESC&$count=true 

应该返回 16 项(或更少),其结构类似于:

{
    "@odata.context": …,
    "@odata.count": 16,
    "value": [
        {
            "@odata.id": null,
            "RunAt": …,
            "RunBy": …,
            "Task": {
                "@odata.id": null,
                "Id": 7,
                "Name": 'TaskName',
                "State": 'Pending',
                "LastChangedAt": …,
                "LastChangedBy": …,
            }
        },
        … 
    ]
}

推荐阅读