在尝试使用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一直低效地获取所有列?
2条答案
按热度按时间kcugc4gi1#
这是
IQueryable
从一开始就存在的根本问题,这么多年后还没有现成的解决方案。问题是
IQueryable
翻译和代码封装/可重用性是互斥的。IQueryable
翻译是基于预先的知识,这意味着查询处理器必须能够“看到”实际代码,然后翻译“已知”的方法/属性。但自定义方法/可计算属性的内容在运行时是不可见的,因此查询处理器通常会失败,或者在支持“客户评估”的有限情况下(EF Core只对最终预测进行评估),它们会生成低效的翻译,检索到比所需数据多得多的数据,就像您的示例一样。总结一下,无论是C#编译器还是BCL都不能帮助解决这个“核心问题”。一些第三方库试图在不同程度上解决这个问题-LinqKit,NeinLinq等等。它们的问题是除了调用
AsExpandable()
,ToInjectable()
等特殊方法外,它们还需要重构现有代码。最近我发现了一个叫做DelegateDecompiler的小gem,它使用另一个叫做Mono.Reflection.Core的包来将方法体反编译成它的lambda表示。
使用它非常简单,安装后只需使用自定义提供的
[Computed]
或[Decompile]
属性标记自定义方法/计算属性(只要确保你使用表达式样式实现而不是代码块),并在IQueryable
链中的某个地方调用Decompile()
或DecompileAsync()
自定义扩展方法。但支持所有其它构造。例如,以扩展方法为例:
(Note:它支持其他方式来告知要反编译哪些方法,例如特定类的所有方法/属性等。)
现在
生产
这种方法(以及其他第三方库)的唯一问题是需要调用自定义扩展方法
Decompile
,以便用自定义提供程序 Package 可查询对象,从而能够预处理最终的查询表达式。如果EF Core允许在其LINQ查询处理管道中插入自定义查询表达式预处理器,从而消除在每个查询中调用自定义方法的需要(这很容易被忘记),并且自定义查询提供程序不适合EF Core特定的扩展,如
AsTracking
、AsNoTracking
、Include
/ThenInclude
,所以应该以他们的名字命名等等。更新(EF核心7.0+):
EF Core 7.0最终添加了Interception to modify the LINQ expression tree功能,因此现在管道代码减少到
原件:
目前有一个开放的问题Please open the query translation pipeline for extension #19748,我试图说服团队添加一个简单的方法来添加表达式预处理器。你可以阅读讨论并投票。
在此之前,我的EF Core 3.1解决方案如下:
你不需要理解这些代码。大部分(如果不是全部的话)都是样板代码,用来支持目前缺失的
IQueryPreprocessor
和AddQueryPreprocesor
(类似于最近添加的拦截器)。如果EF Core将来添加了这些功能,我会更新它。现在,您可以使用它将
DelegateDecompiler
插入EF Core:很多代码只是为了能够调用
在EF核心处理之前,但它就是它。
现在你只需要打个电话
在您的派生上下文中
OnConfiguring
重写,并且您所有的EF Core LINQ查询都将被预处理并注入反编译体。有了你的例子
将自动转换为
因此由EF Core翻译为
这就是我们的目标。
此外,通过投影合成也有效,因此以下查询
原本会生成运行时异常,但现在已成功转换并运行。
yqkkidmi2#
Entity Framework不知道
MapToMinimalItem
方法的任何信息,也不知道如何将其转换为SQL,因此它获取整个实体并在客户端执行Select
。如果您更仔细地查看EF LINQ方法签名,您会发现
IQueryable
使用Func
的Expression
(例如Select
)操作,而不是使用Func
,因为它与IEnumerable
对应,所以底层提供程序可以分析代码并生成所需的内容(本例中为SQL)。所以如果你想把投影代码移到一个单独的方法中,这个方法应该返回
Expression
,这样EF就可以把它转换成SQL。虽然它将有有限的可用性,导致你将无法重用它嵌套的投影,只有在简单的像这样: