Entity Framework:查询子实体

47

看起来我无法从数据库中获取一个父级及其子集的数据。

例如...

db.Parents
.Include(p => p.Children)
.Where(p => p.Children.Any(c => c.Age >= 5))

这将返回所有有5岁或以上孩子的父母,但如果我遍历Parents.Children集合,所有的孩子都会被列出来(不仅仅是那些超过5岁的孩子)。

现在这个查询对我来说是有意义的(我要求包括孩子并且我得到了他们!),但我可以想象,在某些情况下我希望将where子句应用于子集合。

如何获取一个IEnumerable,其中每个父对象都有一个经过过滤的Children集合(Age >= 5)?


你想要的有两种可能性:a)你想要一个父母列表,其中包括年龄大于5岁的孩子;或者b)你想要所有父母的列表以及他们的孩子列表,该列表可以为空,也可以只包含年龄大于5岁的孩子。起初我认为你想要a),现在我认为你想要b)。请澄清一下。 - Andreas
我已经编辑了我的答案,考虑到你在问题中的最后澄清。 - Slauma
仅适用于EF Core 5:https://dev59.com/7FcP5IYBdhLWcg3w6-Op。 - Gert Arnold
4个回答

53

要在单个数据库往返中获取具有筛选的子项集合的父项集合的唯一方法是使用投影。不可能使用急切加载 (Include),因为它不支持过滤,Include 总是加载整个集合。@Daz展示的显式加载方式需要每个父实体一次往返。

例如:

var result = db.Parents
    .Select(p => new
    {
        Parent = p,
        Children = p.Children.Where(c => c.Age >= 5)
    })
    .ToList();

您可以直接使用这个匿名类型对象集合进行操作。(您也可以将其投影到自己的命名类型中,而不是投影到像 Parent 这样的实体中。)

如果您没有禁用更改跟踪(例如使用 AsNoTracking()),EF 的上下文还会自动填充 ParentChildren 集合。在这种情况下,您可以从匿名结果类型中投影出父级(这在内存中完成,不需要数据库查询):

var parents = result.Select(a => a.Parent).ToList();

parents[i].Children将包含每个Parent的经过筛选的子元素。


编辑:在问题中添加了最新的编辑:

我想要一个)有一个年龄大于5岁的孩子的父母列表(并且只包括那些孩子)。

上面的代码将返回所有的父母,并仅包括Age >= 5的孩子,因此如果只有Age <5的孩子,则可能也会包括具有空子元素集合的父母。您可以使用另一个Where子句对父母进行过滤,以仅获取至少有一个Age >= 5的孩子的父母:

var result = db.Parents
    .Where(p => p.Children.Any(c => c.Age >= 5))
    .Select(p => new
    {
        Parent = p,
        Children = p.Children.Where(c => c.Age >= 5)
    })
    .ToList();

当我使用一个暴露了IQueryable的存储库并且没有访问上下文来执行过滤连接时,这种方法对我非常有效。 - Jack Woodward
使用 Func 来避免重复... Func<Child, bool> filterChildren = (c) => c.Age >= 5; - RickardN
2
如果你想在子元素上加载虚拟属性... 有什么想法吗? - Ian

4

3
以您的示例为例,以下内容应该可以满足您的需求。欲了解更多信息,请查看此处
db.Entry(Parents)
.Collection("Children")
.Query().Cast<Child>()
.Where(c => c.Age >= 5))
.Load();

谢谢回复,但我仍然感到困惑!!!我已经编辑了我的原始问题来解释为什么! - ETFairfax
.Query() 方法中添加哪个命名空间,找不到它在 System.Data.Entity.Infrastructure 中。 - sairfan

2
我认为父母和孩子并不是真正适合分开的实体。一个孩子总是可以成为一个父母,通常一个孩子有两个父母(父亲和母亲),所以这不是最简单的情境。但我假设你只有一个简单的1:n关系,就像我使用的以下主从模型。
你需要做的是进行left outer join(那个答案引导了我走上了正确的道路)。这样的连接有点棘手,但以下是代码。
var query = from m in ctx.Masters
            join s in ctx.Slaves
              on m.MasterId equals s.MasterId into masterSlaves
            from ms in masterSlaves.Where(x => x.Age > 5).DefaultIfEmpty()
            select new {
              Master = m,
              Slave = ms
            };

foreach (var item in query) {
  if (item.Slave == null) Console.WriteLine("{0} owns nobody.", item.Master.Name);
  else Console.WriteLine("{0} owns {1} at age {2}.", item.Master.Name, item.Slave.Name, item.Slave.Age);
}

这将转换为使用 EF 4.1 的以下 SQL 语句。
SELECT 
[Extent1].[MasterId] AS [MasterId], 
[Extent1].[Name] AS [Name], 
[Extent2].[SlaveId] AS [SlaveId], 
[Extent2].[MasterId] AS [MasterId1], 
[Extent2].[Name] AS [Name1], 
[Extent2].[Age] AS [Age]
FROM  [dbo].[Master] AS [Extent1]
LEFT OUTER JOIN [dbo].[Slave] AS [Extent2]
ON ([Extent1].[MasterId] = [Extent2].[MasterId]) AND ([Extent2].[Age] > 5)

请注意,在连接的集合上执行附加的年龄where子句而不是在from和select之间执行。
编辑: 如果您想要分层结果,可以通过执行分组来转换平面列表:
var hierarchical = from line in query
                   group line by line.Master into grouped
                   select new { Master = grouped.Key, Slaves = grouped.Select(x => x.Slave).Where(x => x != null) };

foreach (var elem in hierarchical) {
   Master master = elem.Master;
   Console.WriteLine("{0}:", master.Name);
   foreach (var s in elem.Slaves) // note that it says elem.Slaves not master.Slaves here!
     Console.WriteLine("{0} at {1}", s.Name, s.Age);
}

请注意,我使用了匿名类型来存储分层结果。当然,您也可以创建一个特定的类型,如下所示。
class FilteredResult {
  public Master Master { get; set; }
  public IEnumerable<Slave> Slaves { get; set; }
}

然后将该组投影到此类的实例中。如果您需要将这些结果传递给其他方法,这样做会更容易。

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