linq Map到Select中的对象时,EF Core查询SQL中的所有列

nhaq1z21  于 2022-12-15  发布在  其他
关注(0)|答案(2)|浏览(184)

在尝试使用EF Core组织一些数据访问代码时,我注意到生成的查询比以前更糟糕,它们现在查询不需要的列。基本查询只是从一个表中选择并将列的子集Map到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方法中创建的。但是最后两个查询获取所有列,尽管它们与第三个查询做的事情完全相同,只是分别移到了构造函数或扩展方法中。
显然,EF Core无法遵循这段代码并确定如果我将其移出Select方法,它只需要这两列。但我真的希望这样做,以便能够重用Map代码,并使实际的查询代码更易于阅读。我如何提取这种直接的Map代码,而不会使EF Core一直低效地获取所有列?

kcugc4gi

kcugc4gi1#

这是IQueryable从一开始就存在的根本问题,这么多年后还没有现成的解决方案。
问题是IQueryable翻译和代码封装/可重用性是互斥的。IQueryable翻译是基于预先的知识,这意味着查询处理器必须能够“看到”实际代码,然后翻译“已知”的方法/属性。但自定义方法/可计算属性的内容在运行时是不可见的,因此查询处理器通常会失败,或者在支持“客户评估”的有限情况下(EF Core只对最终预测进行评估),它们会生成低效的翻译,检索到比所需数据多得多的数据,就像您的示例一样。
总结一下,无论是C#编译器还是BCL都不能帮助解决这个“核心问题”。一些第三方库试图在不同程度上解决这个问题-LinqKitNeinLinq等等。它们的问题是除了调用AsExpandable()ToInjectable()等特殊方法外,它们还需要重构现有代码。
最近我发现了一个叫做DelegateDecompiler的小gem,它使用另一个叫做Mono.Reflection.Core的包来将方法体反编译成它的lambda表示。
使用它非常简单,安装后只需使用自定义提供的[Computed][Decompile]属性标记自定义方法/计算属性(只要确保你使用表达式样式实现而不是代码块),并在IQueryable链中的某个地方调用Decompile()DecompileAsync()自定义扩展方法。但支持所有其它构造。
例如,以扩展方法为例:

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

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

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

生产

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

这种方法(以及其他第三方库)的唯一问题是需要调用自定义扩展方法Decompile,以便用自定义提供程序 Package 可查询对象,从而能够预处理最终的查询表达式。
如果EF Core允许在其LINQ查询处理管道中插入自定义查询表达式预处理器,从而消除在每个查询中调用自定义方法的需要(这很容易被忘记),并且自定义查询提供程序不适合EF Core特定的扩展,如AsTrackingAsNoTrackingInclude/ThenInclude,所以应该以他们的名字命名等等。

更新(EF核心7.0+):

EF Core 7.0最终添加了Interception to modify the LINQ expression tree功能,因此现在管道代码减少到

using Microsoft.EntityFrameworkCore.Query;
using Microsoft.EntityFrameworkCore.Diagnostics;

namespace Microsoft.EntityFrameworkCore
{
    public static class DelegateDecompilerDbContextOptionsBuilderExtensions
    {
        public static DbContextOptionsBuilder AddDelegateDecompiler(this DbContextOptionsBuilder optionsBuilder)
            => optionsBuilder.AddInterceptors(new DelegateDecompilerQueryPreprocessor());
    }
}

namespace Microsoft.EntityFrameworkCore.Query
{
    using System.Linq.Expressions;
    using DelegateDecompiler;
    public class DelegateDecompilerQueryPreprocessor : IQueryExpressionInterceptor
    {
        Expression IQueryExpressionInterceptor.QueryCompilationStarting(Expression queryExpression, QueryExpressionEventData eventData)
            => DecompileExpressionVisitor.Decompile(queryExpression);
    }
}

原件:

目前有一个开放的问题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核心处理之前,但它就是它。
现在你只需要打个电话

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();

原本会生成运行时异常,但现在已成功转换并运行。

yqkkidmi

yqkkidmi2#

Entity Framework不知道MapToMinimalItem方法的任何信息,也不知道如何将其转换为SQL,因此它获取整个实体并在客户端执行Select
如果您更仔细地查看EF LINQ方法签名,您会发现IQueryable使用FuncExpression(例如Select)操作,而不是使用Func,因为它与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)

相关问题