无法为具有集合属性的类编写正确的 Linq 查询 - 出现 EfCore5 翻译错误

Posted

技术标签:

【中文标题】无法为具有集合属性的类编写正确的 Linq 查询 - 出现 EfCore5 翻译错误【英文标题】:Cannot compose correct Linq query for a class having collection properties - getting EfCore5 translation errors 【发布时间】:2021-11-17 03:05:01 【问题描述】:

我通过查询 DB 获得了以下数据 (IQueryable<Article>) 的平面集合:

ArticleId LanguageName ArticleText ExtraData1 ExtraData2
1 English EngText1 Something1 Something2
1 English EngText2 Another1 Another2
1 French FraText1 Blabla1
2 English EngText2 Ololo1 Blabla2
2 German GerText1 Naturlisch2

现在我需要填写IQueryable<AgregatedArticle>:这个想法是按ArticleId 分组并将重复数据放入嵌套列表中:

public class AgregatedArticle 
    public int ArticleId  get; set; 
    public List<Data> ArticleTexts  get; set; 

    public class Data 
        public string LanguageName  get; set; 
        public string ArticleText  get; set; 
    

很遗憾,我无法做到:我收到各种 EfCore5 翻译错误并且不知道:是我还是 EfCore5 错误或限制。我浪费了 3 天时间尝试不同的方法。请帮助 - 我无法在 Internet 上找到合适的示例。当我尝试填写 ArticleTexts 属性时出现问题。

这是一个简化的例子:

private async Task<IQueryable<LawArticleAggregated>> GetLawArticlesGroupedById(DbSet<LawArticleDetail> dbSet, string userContentLangRestriction = null)

    var dbContext = await GetDbContextAsync();

    var articlesQuery =
        (from articleIds in dbSet.Select(x => x.ArticleId).Distinct()
        from articlesPerId in dbSet
            .Where(x => x.ArticleId == articleIds.ArticleId)
        join askedL in dbContext.Langs
            .Where(l => l.LanguageCode == userContentLangRestriction)
            on
                articlesPerId.LanguageCode
                equals
                askedL.StringValue
            into askedLanguages
        from askedLawLanguage in askedLanguages.DefaultIfEmpty()
        join fallbackL in dbContext.Langs
            .Where(l => l.LanguageCode == CoreConstants.LanguageCodes.English)
        on
            articlesPerId.LanguageCode
            equals
            fallbackL.StringValue
        into fallbackLanguages
        from fallbackLanguage in fallbackLanguages.DefaultIfEmpty()
        select new
        
            ArticleId = articleIds.ArticleId,
            ArticleText = articlesPerId.ArticleText,
            LanguageName = askedLawLanguage.ShortName ?? fallbackLanguage.ShortName
        )
        .OrderBy(x => x.ArticleId).ThenBy(x => x.LanguageName).ThenBy(x => x.ArticleText);

    await articlesQuery.LoadAsync();

    var aggregatedArticleData = articlesQuery.Select(x => new
    
        ArticleId = x.ArticleId,
        ArticleText = x.ArticleText,
        LanguageName = x.LanguageName
    );

    var aggregatedArticles = articlesQuery.Select(x => x.ArticleId).Distinct().Select(x => new ArticleAggregated
    
        ArticleId = x.ArticleId,
        ArticleTexts = aggregatedArticleData.Where(a => a.ArticleId == x.ArticleId)
            .Select(x => new LawArticleAggregated.Data
            
                ArticleText = x.ArticleText,
                LanguageName = x.LanguageName
            ).ToList()
    );

    return aggregatedArticles;

对于此特定代码,异常如下:

自父项以来无法在投影中翻译集合子查询 查询不会投影所有表的关键列 需要在客户端生成结果。这可能发生在 尝试关联无钥匙实体或使用“不同”或 'GroupBy' 操作而不投影所有关键列。

【问题讨论】:

发布您的代码。 IQueryable&lt;T&gt; 代表一个查询。它不包含任何内容,因此无法“填充”。该查询由 EF Core 转换为 SQL。如果某些内容无法转换为 SQL,则会引发异常。你想要做的是不是 GROUP BY 虽然 @panagiotis-kanavos 谢谢 - 我已经添加了代码。是的,我知道它在后台是如何工作的——抱歉不准确。我的意思是 EfCore 无法翻译我的查询并引发各种翻译异常,因此,当稍后使用ToList 时,我无法用数据库数据填充我的代码集合。 您已经使用LoadAsync() 将所有内容加载到内存中,然后丢弃了结果。改用ToListAsync() 并实际使用加载的数据。如果您再次尝试使用articlesQuery,您将执行一个new 查询。您一遍又一遍地加载相同的数据 首先,不要创建平面查询结果集,因为 EF Core GroupBy limitations 无法将其分组以生成您想要的结果。产生这种查询形状的唯一方法是基于平面结果。很难给你好的建议,因为所有这些连接都很难遵循 - 你没有正常的关系/导航属性吗? @panagiotis-kanavos 当我实现整个平面结果时 - 它确实有效。当然,我需要将返回结果从IQueryable 更改为IEnumerable。如果我坚持这种方法,我将需要进行其他更改,因为同时在塑造实体列表数据之后,我们的代码中应用了传统的过滤器和分页,这些代码被转换为 SQL 代码。所以我需要将这部分移动到当前方法中,以获得只需要的页面块......可能有不同的方法来保留 IQueryable......会很好。 【参考方案1】:

我想我已经对您的查询进行了逆向工程。很大的不同是我们不能从这个函数返回IQueryable,但准备好了IEnumerable。所以如果你以后有分页,最好将页面信息传递给函数参数。

private async Task<IEnumerable<LawArticleAggregated>> GetLawArticlesGroupedById(DbSet<LawArticleDetail> dbSet, string userContentLangRestriction = null)

    var dbContext = await GetDbContextAsync();

    var articlesQuery =
        from article in dbSet
        from askedLawLanguage in dbContext.Langs
            .Where(askedLawLanguage => askedLawLanguage.LanguageCode == userContentLangRestriction && article.LanguageCode == askedLawLanguage.StringValue)
            .DefaultIfEmpty()
        from fallbackLanguage in dbContext.Langs
            .Where(fallbackLanguage => fallbackLanguage.LanguageCode == CoreConstants.LanguageCodes.English && article.LanguageCode == fallbackLanguage.StringValue)
            .DefaultIfEmpty()
        select new
        
            ArticleId = article.ArticleId,
            ArticleText = article.ArticleText,
            LanguageName = askedLawLanguage.ShortName ?? fallbackLanguage.ShortName
        ;

    articlesQuery = articlesQuery
        .OrderBy(x => x.ArticleId)
        .ThenBy(x => x.LanguageName)
        .ThenBy(x => x.ArticleText);

    var loaded = await articlesQuery.ToListAsync();

    // group on the client side
    var aggregatedArticles = loaded.GroupBy(x => x.ArticleId)
        .Select(g => new ArticleAggregated
        
            ArticleId = g.Key,
            ArticleTexts = g.Select(x => new LawArticleAggregated.Data
            
                ArticleText = x.ArticleText,
                LanguageName = x.LanguageName
            ).ToList()
        );

    return aggregatedArticles;

【讨论】:

我认为分组需要在服务器上进行,因为需要在它之后应用分页以保持正确的 UI 网格分页:1 ArticleAggregated entry ~ UI 中的 1 个网格行 只有 EF Core 6 才能在服务器上进行这种分组。但如果您将分页信息作为函数的参数传递,我们可以模拟这种行为。【参考方案2】:

我最终得到了以下实现(我“按原样”显示它,没有从第一条消息中简化来演示该方法,对初始变体进行了轻微修改以使用正确的分页):

private async Task<IEnumerable<LawArticleAggregated>> GetLawArticlesGroupedByIdListAsync(
    DbSet<LawArticleDetail> dbSet,
    Expression<Func<IQueryable<LawArticleDetail>, IQueryable<LawArticleDetail>>> filterFunc,
    int skipCount,
    int maxResultCount,
    string userContentLangRestriction = null,
    CancellationToken cancellationToken = default
)

    var dbContext = await GetDbContextAsync();

    var articlesQuery =
        (from articleIds in filterFunc.Compile().Invoke(dbSet).Select(x => new  x.TenantId, x.LawArticleId )
            .Distinct().OrderBy(x => x.TenantId).OrderByDescending(x => x.LawArticleId).Skip(skipCount).Take(maxResultCount)
         from articlesPerId in dbSet
            .Where(x => x.TenantId == articleIds.TenantId && x.LawArticleId == articleIds.LawArticleId)
         join askedL in dbContext.FixCodeValues
            .Where(l =>
                 l.DomainId == CoreConstants.Domains.CENTRAL_TOOLS
                 && l.CodeName == CoreConstants.FieldTypes.LANGUAGE
                 && l.LanguageCode == userContentLangRestriction)
             on
                 articlesPerId.LanguageCode
                 equals
                 askedL.StringValue
             into askedLanguages
         from askedLawLanguage in askedLanguages.DefaultIfEmpty()
         join fallbackL in dbContext.FixCodeValues
            .Where(l =>
                 l.DomainId == CoreConstants.Domains.CENTRAL_TOOLS
                 && l.CodeName == CoreConstants.FieldTypes.LANGUAGE
                 && l.LanguageCode == CoreConstants.LanguageCodes.English)
         on
            articlesPerId.LanguageCode
            equals
            fallbackL.StringValue
         into fallbackLanguages
         from fallbackLanguage in fallbackLanguages.DefaultIfEmpty()
         select new
         
             TenantId = articleIds.TenantId,
             LawArticleId = articleIds.LawArticleId,
             Shortcut = articlesPerId.Shortcut,
             ArticleText = articlesPerId.ArticleText,
             LanguageName = askedLawLanguage.ShortName ?? fallbackLanguage.ShortName
         )
        .OrderBy(x => x.TenantId).ThenByDescending(x => x.LawArticleId).ThenBy(x => x.Shortcut).ThenBy(x => x.LanguageName).ThenBy(x => x.ArticleText);

    var articleList = await articlesQuery.ToListAsync(cancellationToken);

    var aggregatedArticles = articleList.GroupBy(x => new  x.TenantId, x.LawArticleId )
        .Select(g => new LawArticleAggregated
        
            TenantId = g.Key.TenantId,
            LawArticleId = g.Key.LawArticleId,
            ArticleTexts = g.Select(x => new LawArticleAggregated.Data
            
                Shortcut = x.Shortcut,
                ArticleText = x.ArticleText,
                LanguageName = x.LanguageName
            ).ToList()
        );

    return aggregatedArticles;


private async Task<long> GetLawArticlesGroupedByIdCountAsync(
    DbSet<LawArticleDetail> dbSet,
    Expression<Func<IQueryable<LawArticleDetail>, IQueryable<LawArticleDetail>>> filterFunc,
    CancellationToken cancellationToken = default
)

    return await filterFunc.Compile().Invoke(dbSet).GroupBy(x => new  x.TenantId, x.LawArticleId ).LongCountAsync(cancellationToken);

【讨论】:

以上是关于无法为具有集合属性的类编写正确的 Linq 查询 - 出现 EfCore5 翻译错误的主要内容,如果未能解决你的问题,请参考以下文章

linq 问题:查询嵌套集合

如何让 LINQ 返回具有给定属性最大值的对象? [复制]

Linq 查询以返回具有特定属性值的嵌套数组

具有 Linq 和默认值的多个左连接

使用 LINQ 从具有嵌套数组的类中获取子属性值和父属性值

linq查询具有不同属性的多个表