EF:带有where子句的Include

92

正如标题所示,我正在寻找一种与包含(include)结合使用where子句的方法。

这是我的情况:

我负责维护一个代码问题严重的大型应用程序。 改变太多的代码会导致到处都是错误,因此我正在寻找最安全的解决方案。

假设我有一个Bus对象和一个People对象(Bus有一个指向People集合的导航属性)。 在我的查询中,我需要选择所有只有醒着的乘客的巴士。这只是一个简单的虚拟例子。

在当前的代码中:

var busses = Context.Busses.Where(b=>b.IsDriving == true);
foreach(var bus in busses)
{
   var passengers = Context.People.Where(p=>p.BusId == bus.Id && p.Awake == true);
   foreach(var person in passengers)
   {
       bus.Passengers.Add(person);
   }
}

在这段代码之后,上下文被释放,在调用方法中,生成的Bus实体会映射到DTO类(与实体完全一样)。

这段代码导致多次调用数据库,这是不可行的,因此我在MSDN博客上找到了解决方案。

当调试结果时,这个方法效果很好,但当实体映射到DTO(使用AutoMapper)时,我得到一个异常,提示上下文/连接已关闭,对象无法加载。(上下文始终关闭,无法更改这一点:( )

所以,我需要确保所选的乘客已经加载(IsLoaded导航属性也为False)。如果我检查乘客集合,计数也会抛出异常,但乘客集合的“包装相关实体”中还有一个包含我的过滤对象。

是否有办法将这些包装的相关实体加载到整个集合中?(我无法更改automapper映射配置,因为它在整个应用程序中使用)。

还有其他获取活动乘客的方法吗?

任何提示都欢迎...

编辑

Gert Arnold的答案无效,因为数据没有被急切地加载。但是,当我简化代码并删除where时,它被加载了。这真的很奇怪,因为执行SQL在两种情况下都返回所有乘客。因此,在将结果放回实体时肯定存在问题。

Context.Configuration.LazyLoadingEnabled = false;
var buses = Context.Busses.Where(b => b.IsDriving)
        .Select(b => new 
                     { 
                         b,
                         Passengers = b.Passengers
                     })
        .ToList()
        .Select(x => x.b)
        .ToList();

编辑2

经过艰苦的努力,Gert Arnold的答案起作用了!正如Gert Arnold所建议的那样,您需要禁用惰性加载并将其保持关闭状态。这将要求对应用程序进行一些额外的更改,因为之前的开发人员喜欢惰性加载 -_-。


这只是一个在StackOverflow上写的例子,没有智能感知:p 现在已经修复了。 - Beejee
你能否展示一下Bus、People和Passengers类实现中相关部分的样例(例如外键和导航属性)? - Travis J
乘客是一个导航属性。 - Beejee
2
我有点惊讶,考虑到我发现这个问题有多么困难以及它是限制EF查询数据库数据量的好方法,它几乎没有受到关注。难道人们没有看到EF为运行数据库而创建的查询吗? - Ellesedil
@Ellesedil 你的感受是正确的,但那些“长”EF查询只是对人类来说很长。它们实际上非常高效。你会很难编写比EF定期生成的执行计划更快的查询。 - Suamere
b.Passengers如何最终获得正确的值?如果您使用非EF进行此操作,则无法正常工作。您从未设置b.Passengers,只是在匿名类型上设置了Passengers。 - Dave Amour
5个回答

97

这个功能现在已经被添加到Entity Framework core 5中。对于早期版本,您需要使用一个解决方法(请注意,EF6是早期版本)。

Entity Framework 6 解决方法

EF6中,解决方法是先通过投影(new)查询所需的对象,然后让关系修复完成其工作。

您可以通过以下方式查询所需的对象:

Context.Configuration.LazyLoadingEnabled = false;
// Or: Context.Configuration.ProxyCreationEnabled = false;
var buses = Context.Busses.Where(b => b.IsDriving)
            .Select(b => new 
                         { 
                             b,
                             Passengers = b.Passengers
                                           .Where(p => p.Awake)
                         })
            .AsEnumerable()
            .Select(x => x.b)
            .ToList();

这里发生的是,你首先从数据库中获取正在行驶的公交车和等待乘客。然后,AsEnumerable() 从LINQ to Entities切换到LINQ to objects,这意味着公交车和乘客将被实例化,然后在内存中处理。这很重要,因为没有它,EF只会实例化最终的投影 Select(x => x.b),而不是乘客。
现在EF有了这个功能,即关系修复,它负责设置上下文中实例化的所有对象之间的所有关联。这意味着对于每个Bus,现在仅加载其正在等待的乘客。
当你通过ToList获取公交车集合时,你拥有所需乘客的公交车,并且可以使用AutoMapper将它们映射。
这仅适用于禁用延迟加载的情况。否则,EF将在转换为DTO期间访问乘客时为每辆公交车懒惰地加载所有乘客。
有两种方法可以禁用延迟加载。禁用 LazyLoadingEnabled 会在再次启用时重新激活延迟加载。禁用 ProxyCreationEnabled 将创建不能进行懒加载的实体,因此它们在再次启用 ProxyCreationEnabled 后不会开始进行懒加载。当上下文的生命周期超出单个查询时,这可能是最好的选择。
但是... 多对多
如上所述,这个解决方法依赖于关系修复。然而,正如 Slaumahere 中所解释的那样,关系修复无法处理多对多关联。如果 Bus-Passenger 是多对多的,则您唯一能做的就是自己进行修复。
Context.Configuration.LazyLoadingEnabled = false;
// Or: Context.Configuration.ProxyCreationEnabled = false;
var bTemp = Context.Busses.Where(b => b.IsDriving)
            .Select(b => new 
                         { 
                             b,
                             Passengers = b.Passengers
                                           .Where(p => p.Awake)
                         })
            .ToList();
foreach(x in bTemp)
{
    x.b.Pasengers = x.Passengers;
}
var busses = bTemp.Select(x => x.b).ToList();

...整个事情变得更加不吸引人。

第三方工具

有一个叫做EntityFramework.DynamicFilters的库可以使这个过程更加容易。它允许您为实体定义全局过滤器,这些过滤器将在查询实体时自动应用。在您的情况下,这可能看起来像:

modelBuilder.Filter("Awake", (Person p) => p.Awake, true);

现在,如果你这样做...
Context.Busses.Where(b => b.IsDriving)
       .Include(b => b.People)

如果你查看代码,你会发现过滤器应用于所包含的集合。

你还可以启用/禁用过滤器,因此你可以控制它们何时被应用。我认为这是一个非常好的库。

AutoMapper的制造商也有一个类似的库:EntityFramework.Filters

Entity Framework核心解决方法

自2.0.0版本以来,EF-core拥有全局查询过滤器。这些可以用于设置要包含的实体的预定义过滤器。当然,这并不像动态筛选Include那样灵活。 虽然全局查询过滤器是一个很棒的功能,但到目前为止,限制在于过滤器不能包含对导航属性的引用,只能引用查询的根实体。希望在以后的版本中,这些过滤器能够得到更广泛的使用。


1
根据我之前失踪的有关EntityFrameworkDynamicFilters无法用于DB First的评论:https://github.com/zzzprojects/EntityFramework.DynamicFilters/issues/12 - xr280xr
感谢@Gert Arnold的详细阐述!这解决了我的问题,让我再次学到了新东西;)! - Dimitri
1
@Gert Arnold,我该如何将导航属性包含在查询中?假设Passengers具有一个名为Address的实体属性,我该如何将其包含在此查询中?在Select中我没有.Include选项,在Select之前我可以这样做,但那行不通... - Dimitri
1
通过嵌套投影:Passengers = b.Passengers.Where(p => p.Awake).Select(p => new { p, p.Addresses })。这并不优雅,一点也不。 - Gert Arnold
@GertArnold,当然...谢谢你指出来!在循环乘客并将它们添加到公交车中方面,哪种方式最高效(像您的多对多示例或上面建议的方式)?在这种情况下,我不介意优雅,但我选择性能优先。 - Dimitri
1
@Dimitri 关键部分是执行一个SQL查询。任何需要n+1个查询的操作都会迅速降低性能。可能在内存中进行的后处理类型并不重要。 - Gert Arnold

44

现在,EF Core 5.0Filter Include方法现在支持对所包括实体的过滤。

var busses = _Context.Busses
                .Include(b => b.Passengers
                                       .Where(p => p.Awake))
            .Where(b => b.IsDriving);

4
我使用"ToQueryString()"提取了SQL查询语句,但它没有包括".Where(p => p.Awake)"。这是否有效? - rami bin tahin

29

免责声明:我是项目Entity Framework Plus的所有者。

EF+ 查询包含筛选器功能允许过滤相关实体。

var buses = Context.Busses
                   .Where(b => b.IsDriving)
                   .IncludeFilter(x => x.Passengers.Where(p => p.Awake))
                   .ToList();

维基:EF+ 查询IncludeFilter


这个很好用。但是你怎么关闭IncludeFilter呢? - jDave1984
您目前无法关闭它。 - Jonathan Magnan
它可以工作,但比使用AsNotTracking和不必要的数据慢。我没有太多数据(48行与带where子句的1行)。但当我使用IncludeFilter时,我不能将其与Include和AsNoTracking混合使用(受您的库限制)。但我还需要选择6个附加对象。在小测试中,它需要6秒而不是3秒。 - Константин Золин
尝试使用IncludeOptimized,也许这个会更有机会。即使没有过滤器,您仍然可以使用IncludeFilterIncludeOptimized来获取额外的对象。 - Jonathan Magnan
过滤器可以工作(至少能编译和运行),但是包含部分却没有,例如在我的情况下,乘客将成为空集合。 - Flou

2
在我的情况中,Include 是一个 ICollection,我不想返回它们,我只需要获取主要实体并按引用实体进行过滤。(换句话说,Included 实体),我最终做的是这样。这将返回被 InitiativeYears 过滤的 Initiatives 列表。
return await _context.Initiatives
                .Where(x => x.InitiativeYears
                    .Any(y => y.Year == 2020 && y.InitiativeId == x.Id))
                .ToListAsync();

这里InitiativesInitiativeYears之间存在以下关系。

public class Initiative
{
    public int Id { get; set; }
    public string Name { get; set; }
    public ICollection<InitiativeYear> InitiativeYears { get; set; }
}

public class InitiativeYear
{
    public int Year { get; set; }
    public int InitiativeId { get; set; }
    public Initiative Initiative { get; set; }
}

-2

对于仍然对此感到好奇的任何人,EF Core 中有内置功能可实现此操作。在 where 子句中使用 .Any,因此代码类似于以下内容:

_ctx.Parent
    .Include(t => t.Children)
    .Where(t => t.Children.Any(t => /* Expression here */))

7
我已测试过此事,结果并不如你所愿。它只会排除那些没有与表达式匹配的子项的父级。 - Skystrider

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接