【问题标题】:How do you avoid the EF Core 3.0 "Restricted client evaluation" error when trying to order data from a one-to-many join?尝试从一对多连接中排序数据时,如何避免 EF Core 3.0“受限客户端评估”错误?
【发布时间】:2020-07-09 17:38:27
【问题描述】:

我在一个站点中显示了一个表,该表从几个不同的 SQL 表中提取数据。作为参考,我关注this guide 来设置一个可排序的表。简化模型,假设我有一个名为“Data”的主类,它看起来像这样(而 Quotes 类存储 DataID):

namespace MyProject.Models
{
    public class Data
    {
        public int Id { get; set; }
        public string Name { get; set; }
        public int LocationId { get; set; }

        public Models.Location Location { get; set; }
        public IList<Models.Quote> Quotes { get; set; }
    }
}

然后我使用以下代码检索 IQueryable 对象:

    IQueryable<Models.Data> dataIQ = _context.Data
        .Include(d => d.Quotes)
        .Include(d => d.Location);

“报价”表是一对多映射,而位置是一对一映射。如何按 Quotes 表中的值对 IQueryable 对象进行排序?具体来说,当用户单击过滤器按钮时,我正在尝试执行此操作。我尝试对列表中的第一项(保证被填充)执行此操作,但这会抛出我在标题中提到的client-evaluation error。这是我用来应用排序的代码:

//This one throws the client-evaluation error
dataIQ = dataIQ.OrderByDescending(d => d.Quotes[0].QuoteName);

//This one works as expected
dataIQ = dataIQ.OrderByDescending(d => d.Location.LocationName);

【问题讨论】:

    标签: razor entity-framework-core


    【解决方案1】:

    所以你有一个名为Models 的表,其中填充了DataItems 类的对象。你有一张桌子QuotesDataItemsQuotes之间存在一对多关系:每个DataItem有零个或多个Quotes,每个Quote恰好属于一个DataItem,即外键@的DataItem 987654330@指的是。

    此外,每个Quote 都有一个属性QuoteName

    请注意,我将您的 Data 类的标识符更改为 DataItem,因此在引用一个 DataItem 或引用 DataItems 集合时,我更容易用单数和复数名词说话。

    您想以DataItem 的第一个Quote 的属性QuoteName 的升序排列您的DataItems

    我看到两个问题:

    • 如果 DataItem 没有引号怎么办?
    • 是否定义了“First Quote`”这个术语:如果您查看表格,您能说:“这是 Id == 4 的 DataItem 的第一个 Quote”吗?

    这就是原因,通常最好使用virtual ICollection&lt;Quote&gt; 设计一对多关系,然后使用virtual IList&lt;Quote&gt;DataItem[3].Quotes[4] 的值未定义,因此让用户访问索引没有用处。

    但是让我们假设,如果你有一个IQueryable&lt;Quote&gt;,你可以定义一个“第一个报价”。这可以是具有最低 Id 的报价,也可以是具有最旧日期的报价。也许如果它是最常被引用的报价。无论如何,您都可以定义一个扩展方法:

    public static IOrderedQueryable<Quote> ToDefaultQuoteOrder(this IQueryable<Quote> quotes)
    {
         // order by quote Id:
         return quotes.OrderBy(quote => quote.Id);
    
         // or order by QuoteName:
         return quotes.OrderBy(quote => quote.QuoteName);
    
         // or a complex sort order: most mentioned quotes first,
         // then order by oldest quotes first
         return quotes.OrberByDescending(quote => quote.Mentions.Count())
                      .ThenBy(quote => quote.Date)
                      .ThenBy(quote => quote.Id);
    }
    

    如果你希望它被多次使用,创建一个扩展方法才有用。

    既然我们已经在您的报价中定义了一个顺序,那么您可以从每个 DataItem 中获得第一个报价:

    DataItem dataItem = ...
    Quote firstQuote = dataItem.Quotes.ToDefaultQuoteOrder()
                                      .FirstOrDefault();
    

    注意:如果 dataItem 根本没有 Quotes,则不会有 firstQuote,因此您无法获取它的名称。因此,在连接 LINQ 语句时,通常最好只使用FirstOrDefault() 作为序列中的最后一个方法。

    所以你的问题的答案是:

    var result = _context.DataItems.Select(dataItem => new
    {
        DataItem = dataItem,
    
        OrderKey = dataItem.Quotes.ToDefaultQuoteOrder()
                                  .Select(quote => quote.QuoteName)
                                  .FirstOrDefault(),
    })
    .OrderBy(selectionResult => selectionResult.OrderKey)
    .Select(selectioniResult => selectionResult.Data);
    

    扩展方法的好处在于您隐藏了引号的排序方式。如果您想更改这一点,而不是按 ID 排序,而是按最旧的报价日期,则用户不必更改。

    最后一点:使用 Include 作为 Select 的快捷方式通常不是一个好主意。如果 DataItem [4] 有 1000 个 Quotes,那么它的每一个 Quote 都会有一个值为 4 的 DataItemId。发送这个值 4 超过一千次是相当浪费的。使用 Select 时,您只能传输您实际计划使用的属性:

    .Select(dataItem => new
    {
        // Select only the data items that you plan to use:
        Id = dataItem.Id,
        Name = dataItem.Name,
        ...
    
        Quotes = dataItem.Quotes.ToDefaultQuoteOrder().Select(quote => new
        {
             // again only the properties that you plan to use:
             Id = quote.Id,
             ...
    
             // not needed, you know the value:
             // DataItemId = quote.DataItemId,
        })
        .ToList(),
     });
    

    在实体框架中始终使用 Select 来选择数据并仅选择您真正计划使用的属性。仅当您计划更改/更新包含的数据时才使用 include。

    当然不要使用 Include,因为它可以节省您的打字时间。再说一遍:当你必须多次做某事时,为它创建一个过程:

    作为扩展方法:

    public static IQueryable<MyClass> ToPropertiesINeed(this IQueryable<DataItem> source)
    {
        return source.Select(item => new MyClass
        {
             Id = item.Id,
             Name = item.Name,
             ...
    
             Quotes = item.Quotes.ToDefaultQuoteOrder.Select(...).ToList(),
        });
    }
    

    用法:

    var result = var result = _context.DataItems.Where(dataItem => ...)
                                                .ToPropertiesINeed();
    

    Select 的好处在于您将数据库的结构与实际返回的数据分开。如果您的数据库结构发生变化,您的类的用户将不必看到这一点。

    【讨论】:

    • 非常感谢您的详细回复!这非常有用,并且回答了一些我什至没有要求启动的问题。
    【解决方案2】:

    好的,我想我想通了(至少部分**)。我相信我得到了错误,因为我所拥有的实际上只是 Linq 查询的语法不正确——也就是说,我试图在一个不存在的表的查询中使用列表成员(也许?)

    更正我能够想出的语法,这适用于我当前的目的。缺点是它仅按链接中的第一项排序。我不确定您如何针对多个项目执行此操作——有兴趣看看其他人是否有想法

    dataIQ = dataIQ.OrderByDescending(d => d.Quotes.FirstOrDefault().QuoteName);
    

    **编辑:确认这只是部分解决了我的问题。如果我尝试访问 Quotes 的子对象,我仍然会收到原始错误。有人对如何避免此错误有建议吗?下面的例子仍然会触发错误:

     IQueryable<Models.Data> dataIQ = _context.Data
            .Include(d => d.Quotes).ThenInclude(q => q.Owner)
            .Include(d => d.Location);
    
    dataIQ = dataIQ.OrderByDescending(d => d.Quotes.FirstOrDefault().Owner.OwnerName);
    

    【讨论】:

      猜你喜欢
      • 1970-01-01
      • 1970-01-01
      • 2020-11-27
      • 2020-05-02
      • 2020-02-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 2021-05-03
      相关资源
      最近更新 更多