包括在where从句中

本文关键字:where 包括 | 更新日期: 2023-09-27 18:15:57

正如标题所示,我正在寻找一种方法来做一个where子句与include相结合。

以下是我的情况:我负责支持一个充满代码异味的大型应用程序。修改太多代码会导致bug无处不在,所以我正在寻找最安全的解决方案。

假设我有一个对象Bus和一个对象People(Bus有一个导航道具Collection of 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);
   }
}

在这段代码之后,Context被处理,并且在调用方法中产生的总线实体被映射到DTO类(Entity的100%副本)。

这段代码导致多次调用DB,这是一个No-Go,所以我在MSDN博客上找到了这个解决方案

这在调试结果时工作得很好,但是当实体映射到DTO(使用AutoMapper)时,我得到一个异常,即上下文/连接已关闭,对象无法加载。(上下文总是关闭的,不能改变这个:()

所以我需要确保Selected Passengers已经加载(IsLoaded on navigation属性也是False)。如果我检查乘客集合,Count也会抛出异常,但是乘客集合上还有一个名为"包装相关实体"的集合,其中包含我过滤的对象。

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

有其他方法获得活跃的乘客吗?

欢迎任何提示…

编辑

Gert Arnold的答案不工作,因为数据没有被急切加载。但是当我简化它并删除它的加载位置时。这真的很奇怪,因为在这两种情况下,execute 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();

Edit2

经过一番努力,答案是格特·阿诺德的作品!正如Gert Arnold建议的那样,你需要禁用惰性加载并保持它关闭。这将要求对应用程序进行一些额外的更改,因为以前的开发人员喜欢延迟加载-_-

包括在where从句中

此功能现已添加到实体框架核心5。对于较早的版本,您需要一个变通方法(请注意,EF6是较早的版本)。

实体框架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到实体切换到LINQ到对象,这意味着公共汽车和乘客将被物化,然后在内存中处理。这一点很重要,因为没有它,EF只会实现最终的投影,Select(x => x.b),而不是乘客。

现在EF有这个特性关系固定,它负责设置在上下文中物化的对象之间的所有关联。这意味着对于每个Bus,现在只装载其清醒的乘客。

当你得到ToList的公共汽车集合时,你就有了你想要的乘客,你可以用AutoMapper映射它们。

这只在惰性加载被禁用时起作用。否则,在转换为dto时,EF将在每个总线访问所有乘客时延迟加载

有两种方法可以禁用惰性加载。禁用LazyLoadingEnabled将在再次启用时重新激活延迟加载。禁用ProxyCreationEnabled将创建无法延迟加载自身的实体,因此它们不会在再次启用ProxyCreationEnabled后开始延迟加载。当上下文的生存时间比单个查询长时,这可能是最佳选择。

但是…多对多的

如前所述,这种解决方法依赖于关系的修复。然而,正如Slauma在这里解释的那样,关系修复不适用于多对多关联。如果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。过滤器

实体框架核心解决方案

从2.0.0版本开始,EF-core有全局查询过滤器。这些可用于在要包含的实体上设置预定义筛选器。当然,这不能提供与动态过滤Include相同的灵活性。尽管全局查询过滤器是一个很好的特性,但目前的限制是过滤器不能包含对导航属性的引用,只能包含对查询根实体的引用。希望在以后的版本中,这些过滤器将获得更广泛的使用。

Now EF Core 5.0的Filter Include方法现在支持对包含的实体进行过滤

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

免责声明:我是项目实体框架Plus的所有者

EF+ Query IncludeFilter功能允许过滤相关实体

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

Wiki: EF+ Query IncludeFilter

在我的情况下,IncludeICollection,也不想返回它们,我只需要获得主要实体,但被引用的实体过滤。(换句话说,Included实体),我最终做的是这样的。这将返回Initiatives但被InitiativeYears过滤的列表

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; }
}

对于任何仍然对此感到好奇的人。在EF Core中有这样的内置功能。在where子句中使用。any这样代码就会像这样

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