gpt4 book ai didi

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

转载 作者:行者123 更新时间:2023-12-03 16:51:10 26 4
gpt4 key购买 nike

在尝试使用 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只要直接在 Select 方法中创建 DTO,它也可以工作。但是最后两个查询获取所有列,即使它们与第三个查询完全相同,只是分别移动到构造函数或扩展方法。

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

最佳答案

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

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

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

最近我发现了一个小 gem ,叫做 DelegateDecompiler ,它使用另一个名为 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 查询处理管道中插入自定义查询表达式预处理器,那就太好了,从而消除了在每个查询中调用自定义方法的需要,这很容易被遗忘,而且自定义查询提供程序也不能很好地与 EF 配合使用核心特定扩展,如 AsTracking , AsNoTracking , Include/ ThenInclude ,所以它真的应该在他们之后调用等等。

目前有一个 Unresolved 问题 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 核心:
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 查询都将被预处理并注入(inject)反编译的主体。

有你的例子
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();

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

关于c# - EF Core 在映射到 Select 中的对象时查询 SQL 中的所有列,我们在Stack Overflow上找到一个类似的问题: https://stackoverflow.com/questions/62115690/

26 4 0
Copyright 2021 - 2024 cfsdn All Rights Reserved 蜀ICP备2022000587号
广告合作:1813099741@qq.com 6ren.com