Entity Framework如何按顺序加载子集合?

42

我有两个表,一个父表和一个子表。子表有一个sortorder列(数字值)。由于EF不支持持久化包含sort order的IList,而且不公开sortorder(见: Entity Framework persisting child collection sort order),因此我的子类也具有SortOrder属性,以便我可以按顺序存储孩子们。

与引用问题的作者相反,我总是尝试排序加载的子项。因此,如果我加载一个父实例,我期望子集合按sort order排序。如何使用Code First Fluent API和POCO实现这种行为?

提示:不能在子集合上调用.Sort(...)。


https://github.com/aspnet/EntityFrameworkCore/issues/15171 - juFo
4个回答

51

你不能直接实现它,因为EF中的eager或lazy loading都不支持排序或过滤。

你的选项是:

  • 在从数据库加载数据后,在应用程序中对数据进行排序
  • 执行单独的查询来加载子记录。一旦使用了单独的查询,你可以使用OrderBy

第二个选项可以与显式加载一起使用:

var parent = context.Parents.First(...);
var entry = context.Entry(parent);
entry.Collection(e => e.Children)
     .Query()
     .OrderBy(c => c.SortOrder)
     .Load();

2
请不要忘记添加以下using语句:system.data.entity和system.linq。 - Eric
不要忘记对父对象进行空值检查。 - Joe H
1
十年后...你的答案似乎仍然适用于Entity Framework Core。这真的很遗憾,因为NHibernate支持直接在映射中定义排序顺序。 - Jonas Benz

34

你可以在单个查询中高效地完成此操作,语法只是有点别扭:

var groups = await db.Parents
    .Where(p => p.Id == id)
    .Select(p => new
        {
            P = p,
            C = p.Children.OrderBy(c => c.SortIndex)
        })
    .ToArrayAsync();

// Query/db interaction is over, now grab what we wanted from what was fetched

var model = groups
    .Select(g => g.P)
    .FirstOrDefault();

解释

async 注意事项

这里我使用了 async 扩展,你可能也应该使用它们,但如果需要同步查询而不影响有效的子元素排序,则可以去掉 await/async

第一部分

默认情况下,从数据库中获取的所有 EF 对象都是“跟踪”的。此外,EF 的等效于 SQL 的 Select 是围绕匿名对象设计的,你在上面看到我们选择了它们。当创建匿名对象时,分配给 PC 的对象都是被跟踪的,这意味着它们的关系被记录并且它们的状态由 EF 更改跟踪器维护。由于 CP 中的子代列表,即使你没有明确要求它们在匿名对象中相关联,EF 仍然会将它们作为这个子集合加载,因为它在模式中看到了这种关系。

要了解更多信息,你可以将上述内容分成两个单独的查询,在完全不同的 Db 调用中分别加载父对象和子列表。EF 更改跟踪器将注意到并将子节点加载到父对象中。

第二部分

我们已经欺骗 EF 返回了有序子项。现在我们只获取父对象 - 它的子元素仍将以我们想要的顺序附加。

空值和表作为集合

这里存在一个笨拙的 2 步骤,主要是为了遵循最佳实践,处理空值的情况;它需要完成两件事:

  • 将数据库中的内容视为集合,直到最后一刻。

  • 避免 null 异常。

换句话说,最后一部分也可以是:

var model = groups.First().P;

但是如果该对象在数据库中不存在,将导致空引用异常。 C# 6 将引入另一种替代方法,即空属性合并运算符 - 因此在未来,您可以使用以下代码替换最后一部分:

var model = groups.FirstOrDefault()?.P;

2
这对我很有效!太棒了!经过几个相关问题后,终于有一个好的答案可用!在我看来,X181应该将其标记为正确答案。 - ctb
我遇到了这个错误:ObjectContext 实例已被处理并且不能再用于需要连接的操作。 我在你的代码中只是将 FirstOrDefault() 替换为了 ToList(),有什么想法出了问题吗? - Jo Smo
@JoSmo 听起来你在这个受限样本之外遇到了问题。你很可能在其他线程中释放了 DbContext,导致此代码失败。你的问题最好作为一个新的 StackOverflow 问题而不是在评论中讨论 - 当你这样做时,请发布完整的代码,包括任何使用语句和线程。 - Chris Moschini
2
@vtortola 增加了更多的解释来尝试解释这里发生了什么,希望能有所帮助。 - Chris Moschini
5
有人知道这是否适用于EF7吗?它对我不起作用,我猜这就是原因。 - Kevin Brey
显示剩余6条评论

0
在 EF Core 5 和 6 中:
var results = await DbSet.Where(t => <your condition>)
                         .Include(t => t.Children.OrderBy(child => child.YourProperty))
                         .ToListAsync(cancellationToken);

0
除了需要排序外,我还需要限制子元素的结果。我是这样做的:
var transactions = await _context.Transaction
            .Include(x => x.User)
            .OrderByDescending(x => x.CreatedAt)
            .Where(x => x.User.Id == _tenantInfo.UserId)
            .Take(10)
            .ToListAsync();

var viewmodel = _mapper.Map<UserViewModel>(transactions.First().User);

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