EF Core 在映射到 Select 中的对象时查询 SQL 中的所有列

Posted

技术标签:

【中文标题】EF Core 在映射到 Select 中的对象时查询 SQL 中的所有列【英文标题】:EF Core queries all columns in SQL when mapping to object in Select 【发布时间】:2020-05-31 11:41:57 【问题描述】:

在尝试使用 EF Core 组织一些数据访问代码时,我注意到生成的查询比以前更糟糕,它们现在查询了不需要的列。基本查询只是从一个表中选择并将列的子集映射到 DTO。但是在重写之后,现在所有列都被提取了,而不仅仅是 DTO 中的列。

我创建了一个最小示例,其中包含一些显示问题的查询:

ctx.Items.ToList();
// SELECT i."Id", i."Property1", i."Property2", i."Property3" FROM "Items" AS i


ctx.Items.Select(x => new

  Id = x.Id,
  Property1 = x.Property1

).ToList();
// SELECT i."Id", i."Property1" FROM "Items" AS i

ctx.Items.Select(x => new MinimalItem

  Id = x.Id,
  Property1 = x.Property1

).ToList();
// SELECT i."Id", i."Property1" FROM "Items" AS i

ctx.Items.Select(
  x => x.MapToMinimalItem()
).ToList();
// SELECT i."Id", i."Property1", i."Property2", i."Property3" FROM "Items" AS i

ctx.Items.Select(
  x => new MinimalItem(x)
).ToList();

// SELECT i."Id", i."Property1", i."Property2", i."Property3" FROM "Items" AS i

对象的定义如下:

  public class Item
  
    public int Id  get; set; 
    public string Property1  get; set; 
    public string Property2  get; set; 
    public string Property3  get; set; 

  

  public class MinimalItem
  
    public MinimalItem()  

    public MinimalItem(Item source)
    
      Id = source.Id;
      Property1 = source.Property1;
    
    public int Id  get; set; 
    public string Property1  get; set; 
  

  public static class ItemExtensionMethods
  
    public static MinimalItem MapToMinimalItem(this Item source)
    
      return new MinimalItem
      
        Id = source.Id,
        Property1 = source.Property1
      ;
    
  

第一个查询按预期查询所有列,第二个带有匿名对象的查询只查询选定的查询,一切正常。使用我的MinimalItem DTO 也可以,只要它是直接在 Select 方法中创建的。但是最后两个查询获取所有列,即使它们与第三个查询完全相同,只是分别移动到构造函数或扩展方法。

显然,如果我将其移出 Select 方法,EF Core 无法遵循此代码并确定它只需要两列。但我真的很想这样做以便能够重用映射代码,并使实际的查询代码更易于阅读。如何提取这种简单的映射代码,而不会使 EF Core 一直低效地获取所有列?

【问题讨论】:

您的查询正在返回整个类 ITEM。 @jdweng 最后两个在 Select 中返回一个 MinimalItem。就像第三个有效一样。这是最后两个查询中toList() 向我显示的签名 VS Code:List<MinimalItem> IEnumerable<MinimalItem>.ToList<MinimalItem>() 如果您只想要一些列,那么您需要一个选择来指定要返回的列。您没有选择,因此将返回所有列。查看以下返回的一些列:SELECT i."Id", i."Property1" (1) 您使用的是哪个 EF Core 版本? (2) 是否允许使用 3rd 方开源包? @IvanStoev 最新的 .NET Core 3.1。如果可能的话,我宁愿避免使用第三方包来解决这种核心问题,尤其是像 Automapper 这样更复杂和“神奇”的东西。但我并不坚决反对为此使用包。 【参考方案1】:

这是IQueryable 从一开始就存在的根本问题,经过这么多年没有开箱即用的解决方案。

问题在于IQueryable 翻译和代码封装/可重用性是相互排斥的。 IQueryable 翻译基于预先的知识,这意味着查询处理器必须能够“看到”实际代码,然后翻译“已知”的方法/属性。但是自定义方法/可计算属性的内容在运行时不可见,因此查询处理器通常会失败,或者在它们支持“客户端评估”的有限情况下(EF Core 仅针对最终预测)它们会生成低效的翻译,从而检索到很多比您的示例中需要的更多数据。

回顾一下,C# 编译器和 BCL 都不能帮助解决这个“核心问题”。一些第 3 方图书馆正试图以不同的程度解决它 - LinqKit、NeinLinq 和类似的。它们的问题在于,它们需要重构现有代码,以调用 AsExpandable()ToInjectable() 等特殊方法。

最近我发现了一个名为 DelegateDecompiler 的小 gem,它使用另一个名为 Mono.Reflection.Core 的包将方法体反编译为其 lambda 表示。

使用起来非常简单。安装后您需要做的就是用自定义提供的[Computed][Decompile] 属性标记您的自定义方法/计算属性(只要确保您使用表达式样式实现而不是代码块),然后调用Decompile()DecompileAsync() IQueryable 链中某处的自定义扩展方法。它不适用于构造函数,但支持所有其他构造。

以你的扩展方法为例:

public static class ItemExtensionMethods

    [Decompile] // <--
    public static MinimalItem MapToMinimalItem(this Item source)
    
        return new MinimalItem
        
            Id = source.Id,
            Property1 = source.Property1
        ;
    

(注意:它支持其他方式来告诉哪些方法要反编译,例如特定类的所有方法/属性等)

现在

ctx.Items.Decompile()
    .Select(x => x.MapToMinimalItem())
    .ToList();

生产

// SELECT i."Id", i."Property1" FROM "Items" AS i

这种方法(和其他 3rd 方库)的唯一问题是需要调用自定义扩展方法 Decompile,以便使用自定义提供程序包装可查询对象,以便能够预处理最终查询表达式。

如果 EF Core 允许在其 LINQ 查询处理管道中插入自定义查询表达式预处理器,那就太好了,这样就无需在每个查询中调用自定义方法,这很容易被遗忘,而且自定义查询提供程序也不起作用与 AsTrackingAsNoTrackingInclude/ThenInclude 等 EF Core 特定的扩展很好,所以它真的应该在它们之后被调用 等等。

目前有一个未解决的问题Please open the query translation pipeline for extension #19748,我试图说服团队添加一种简单的方法来添加表达式预处理器。您可以阅读讨论并投票。

在此之前,这是我的 EF Core 3.1 解决方案:

using System;
using System.Collections.Generic;
using System.Linq;
using System.Linq.Expressions;
using Microsoft.EntityFrameworkCore.Infrastructure;
using Microsoft.EntityFrameworkCore.Query;
using Microsoft.Extensions.DependencyInjection;

namespace Microsoft.EntityFrameworkCore

    public static partial class CustomDbContextOptionsExtensions
    
        public static DbContextOptionsBuilder AddQueryPreprocessor(this DbContextOptionsBuilder optionsBuilder, IQueryPreprocessor processor)
        
            var option = optionsBuilder.Options.FindExtension<CustomOptionsExtension>()?.Clone() ?? new CustomOptionsExtension();
            if (option.Processors.Count == 0)
                optionsBuilder.ReplaceService<IQueryTranslationPreprocessorFactory, CustomQueryTranslationPreprocessorFactory>();
            else
                option.Processors.Remove(processor);
            option.Processors.Add(processor);
            ((IDbContextOptionsBuilderInfrastructure)optionsBuilder).AddOrUpdateExtension(option);
            return optionsBuilder;
        
    


namespace Microsoft.EntityFrameworkCore.Infrastructure

    public class CustomOptionsExtension : IDbContextOptionsExtension
    
        public CustomOptionsExtension()  
        private CustomOptionsExtension(CustomOptionsExtension copyFrom) => Processors = copyFrom.Processors.ToList();
        public CustomOptionsExtension Clone() => new CustomOptionsExtension(this);
        public List<IQueryPreprocessor> Processors  get;  = new List<IQueryPreprocessor>();
        ExtensionInfo info;
        public DbContextOptionsExtensionInfo Info => info ?? (info = new ExtensionInfo(this));
        public void Validate(IDbContextOptions options)  
        public void ApplyServices(IServiceCollection services)
            => services.AddSingleton<IEnumerable<IQueryPreprocessor>>(Processors);
        private sealed class ExtensionInfo : DbContextOptionsExtensionInfo
        
            public ExtensionInfo(CustomOptionsExtension extension) : base(extension)  
            new private CustomOptionsExtension Extension => (CustomOptionsExtension)base.Extension;
            public override bool IsDatabaseProvider => false;
            public override string LogFragment => string.Empty;
            public override void PopulateDebugInfo(IDictionary<string, string> debugInfo)  
            public override long GetServiceProviderHashCode() => Extension.Processors.Count;
        
    


namespace Microsoft.EntityFrameworkCore.Query

    public interface IQueryPreprocessor
    
        Expression Process(Expression query);
    

    public class CustomQueryTranslationPreprocessor : RelationalQueryTranslationPreprocessor
    
        public CustomQueryTranslationPreprocessor(QueryTranslationPreprocessorDependencies dependencies, RelationalQueryTranslationPreprocessorDependencies relationalDependencies, IEnumerable<IQueryPreprocessor> processors, QueryCompilationContext queryCompilationContext)
            : base(dependencies, relationalDependencies, queryCompilationContext) => Processors = processors;
        protected IEnumerable<IQueryPreprocessor> Processors  get; 
        public override Expression Process(Expression query)
        
            foreach (var processor in Processors)
                query = processor.Process(query);
            return base.Process(query);
        
    

    public class CustomQueryTranslationPreprocessorFactory : IQueryTranslationPreprocessorFactory
    
        public CustomQueryTranslationPreprocessorFactory(QueryTranslationPreprocessorDependencies dependencies, RelationalQueryTranslationPreprocessorDependencies relationalDependencies, IEnumerable<IQueryPreprocessor> processors)
        
            Dependencies = dependencies;
            RelationalDependencies = relationalDependencies;
            Processors = processors;
        
        protected QueryTranslationPreprocessorDependencies Dependencies  get; 
        protected RelationalQueryTranslationPreprocessorDependencies RelationalDependencies  get; 
        protected IEnumerable<IQueryPreprocessor> Processors  get; 
        public QueryTranslationPreprocessor Create(QueryCompilationContext queryCompilationContext)
            => new CustomQueryTranslationPreprocessor(Dependencies, RelationalDependencies, Processors, queryCompilationContext);
    

您无需了解该代码。其中大部分(如果不是全部)是一个样板管道代码,以支持当前缺少的IQueryPreprocessorAddQueryPreprocesor(类似于最近添加的拦截器)。如果 EF Core 将来添加该功能,我会对其进行更新。

现在您可以使用它将DelegateDecompiler 插入 EF Core:

using System.Linq.Expressions;
using Microsoft.EntityFrameworkCore.Query;
using DelegateDecompiler;

namespace Microsoft.EntityFrameworkCore

    public static class DelegateDecompilerDbContextOptionsExtensions
    
        public static DbContextOptionsBuilder AddDelegateDecompiler(this DbContextOptionsBuilder optionsBuilder)
            => optionsBuilder.AddQueryPreprocessor(new DelegateDecompilerQueryPreprocessor());
    


namespace Microsoft.EntityFrameworkCore.Query

    public class DelegateDecompilerQueryPreprocessor : IQueryPreprocessor
    
        public Expression Process(Expression query) => DecompileExpressionVisitor.Decompile(query);
    

大量代码只是为了能够调用

DecompileExpressionVisitor.Decompile(query)

在 EF Core 处理之前,但现在你只需要调用

optionsBuilder.AddDelegateDecompiler();

在您的派生上下文中 OnConfiguring 覆盖,您的所有 EF Core LINQ 查询都将被预处理并注入反编译的主体。

举个例子

ctx.Items.Select(x => x.MapToMinimalItem())

会自动转换成

ctx.Items.Select(x => new

    Id = x.Id,
    Property1 = x.Property1

EF Core 将其翻译成

// SELECT i."Id", i."Property1" FROM "Items" AS I

这是目标。

此外,在投影上作曲也可以,所以下面的查询

ctx.Items
    .Select(x => x.MapToMinimalItem())
    .Where(x => x.Property1 == "abc")
    .ToList();

原本会产生运行时异常,但现在翻译并成功运行。

【讨论】:

【参考方案2】:

Entity Framework 不知道您的 MapToMinimalItem 方法以及如何将其转换为 SQL,因此它会获取整个实体并在客户端执行 Select

如果您仔细查看 EF LINQ 方法签名,您会发现 IQueryable 使用 ExpressionFunc(例如 Select)而不是 Funcs IEnumerable 对应,因此底层提供者可以分析代码并生成所需的内容(本例中为 SQL)。

因此,如果您想将投影代码移动到单独的方法中,此方法应返回Expression,以便 EF 将其转换为 SQL。例如:

public static class ItemExtensionMethods

    public static readonly Expression<Func<Item, MinimalItem>> MapToMinimalItemExpr = 
        source => new MinimalItem
        
            Id = source.Id,
            Property1 = source.Property1
        ;

虽然它的可用性有限,导致您无法重用它的嵌套投影,但只能像这样简单:

ctx.Items.Select(ItemExtensionMethods.MapToMinimalItemExpr)

【讨论】:

感谢您为我指明了正确的方向,让我知道它的工作原理。我玩了一下这个,感觉更自然的是把整个 Select 放到一个扩展方法中,避免直接处理 Expressions。无论如何,我都不能为 LINQ 之外的情况重用映射代码。 @MadScientist 1) 实际上你可以,只需在表达式上调用 Compile 并将其保存在某处:class ItemExtensionMethods .... MapToMinimalItem = ItemExtensionMethods.MapToMinimalItemExpr.Compile(); 2) 我不喜欢它,但 Automapper 可以选择重用它的映射在选择子句中。 @GuruStron 可以通过在 Select 之前使用 AsQueryable 强制 IQueryable 用于嵌套投影

以上是关于EF Core 在映射到 Select 中的对象时查询 SQL 中的所有列的主要内容,如果未能解决你的问题,请参考以下文章

EF Core中的多对多映射如何实现?

asp.net core, Ef core : 在运行时动态映射存储库和服务

带有 EF Core 的 ASP.NET Core - DTO 集合映射

Dotnet EF Core 2.1 在查询小数属性时抛出 QueryClientEvaluationWarning

如何在 EF / EF Core 中的第二个表上实现具有某些条件的左连接?

将类别父 ID 自引用表结构映射到 EF Core 实体