Entity Framework v6.1 高效地加载深度相关实体并在其上进行查询

3
我有以下实体:分类(Category)、主题(Topic)、帖子(Post)和成员(Member)。它们之间的关系如下:
  • 分类拥有一组主题
  • 主题拥有一组帖子
  • 帖子拥有一个成员
以下是类:
public class Category
{
    public Guid Id { get; set; }
    public string Name { get; set; }
    public string Description { get; set; }
    public virtual IList<Topic> Topics { get; set; }
}

public class Topic
{
    public Guid Id { get; set; }
    public string Name { get; set; }
    public DateTime CreateDate { get; set; }
    public virtual Category Category { get; set; }
    public virtual IList<Post> Posts { get; set; }
    public virtual MembershipUser User { get; set; }
}

public class Post
{
    public Guid Id { get; set; }
    public string PostContent { get; set; }
    public DateTime DateCreated { get; set; }
    public DateTime DateEdited { get; set; }
    public virtual Topic Topic { get; set; }
    public virtual MembershipUser User { get; set; }
}

public class MembershipUser
{
    public Guid Id { get; set; }
    public string UserName { get; set; }

    etc....

}

我希望能够有效地进行以下查询:

  1. 获取包括发表该帖子的会员在内的某一类别(按CategoryId)中的最新帖子
  2. 获取包括发表该帖子的会员在内的某一主题(按TopicId)中的最新帖子

我一直在使用包括()函数的以下方法-但我想知道是否有更有效的方法来完成这个操作...?

查询1

_context.Category
     .Where(x => x.Id == categoryId)
     .Include(x => x.Topics.Select(p => p.Posts.Select(u => u.User)))
     .SelectMany(x => x.Topics)
     .SelectMany(x => x.Posts)
     .OrderByDescending(x => x.DateCreated)
     .FirstOrDefault();

查询 2

_context.Topic
       .Where(x => x.Id == topicId)
       .Include(x => x.Posts.Select(u => u.User))
       .SelectMany(x => x.Posts)
       .OrderByDescending(x => x.DateCreated)
       .FirstOrDefault();

非常感谢任何帮助或指导。

2个回答

4
如果您正在寻找高效的性能,可能会对编写一个非常简单的MARS存储过程来获取所需数据感兴趣。您可以在每个结果集上使用Translate函数来实现模型对象的物化。Entity Framework将自动修复导航属性。

http://msdn.microsoft.com/en-us/data/jj691402.aspx

如果您不想创建proc,执行多个简单查询通常更有效。我经常使用内存中的Id列表过滤linq到实体查询,如下所示:qry.where(x=>list.contains(x.Id))。
截至2014年9月21日的编辑
大多数开发人员认为高效查询是指执行速度快且仅返回所需数据的查询。这基本上是正确的。然而,高效的数据访问层是重复使用少量快速执行的查询。有时开发人员试图使每个单独的查询尽可能高效,而没有意识到他们正在导致sql server管理过多的执行计划,从而减慢了整体性能。我建议您尝试为给定表格坚持使用两到三种方法。我会从返回一个主题及其相关数据的查询和返回您需要的数据列表的主题列表开始。
以下方法将放在您的DataContext类中:
public Topic GetTopic(int topicId) 
{
      return this.Topics.Include("Posts.User").Single(x => x.Id = topicId);
}

这可以放在你的Topic类中:

public Post GetMostRecentPost()
{
    return this.Posts.OrderByDescending(x => x.DateCreated).FirstOrDefault();
}

如果您只需要获取最新的帖子,而且从未需要查询所有主题及其帖子,那么您可以在上下文中使用以下查询。

public Post GetMostRecentPost(int topicId)
{
  return this.Posts.Include(x => x.Topic).Include(x=>x.User).where(x => x.TopicId == topicId).OrderByDescending(x => x.DateCreated).FirstOrDefault();
}

作为一个基本的经验规则,如果你想返回一个Post,最好从context.Post开始查询并尝试在此基础上构建你的查询。尽量避免使用投影查询(如select或selectmany),除非你打算返回匿名对象并愿意执行SQL分析以确保查询看起来符合预期。

重新查看您的查询后,我意识到您需要一个非常简单的查询。三个表并不算多。请尝试这个: - Chris Perry
1
context.Topics.Include("Posts.User").Single(x=>x. Id=topicId).selectmany(x=>x.Posts).orderbydescending(x=>x.DateCreated).FirstOrDefault() - Chris Perry
记住的重要事情是尽可能保持查询简单,并在检索数据后在内存中进行投影。如果上述查询不起作用,请告诉我。 - Chris Perry
首先让我们看一下First()和FirstOrDefault()之间的区别。如果没有返回结果,First()会抛出异常。这对于你的使用方式很重要,因为你没有检查null,可能会得到一个不太描述性的null引用异常。First()和Single()之间的区别在于,如果从数据库返回多个结果,Single()会抛出异常。First()也可以工作,但会忽略这种异常情况。我认为Single()看起来更正确,因为我们期望只有一个结果。 - Chris Perry
谢谢您的评论。我找到了这个,正是我所怀疑的。Includes() 不够高效 :( 开始重构 http://mikee.se/Archive.aspx/Details/entity_framework_pitfalls,_include_20140101 - YodasMyDad
显示剩余4条评论

2
首先,您应该从测量当前查询时间开始。考虑到FirstOrDefault(),我希望这个查询能够快速运行。
我通常使用Sql Profiler来进行这些操作。在Web应用程序中,我通常还会使用StackExchange.MiniProfiler或Glimpse。两者都可以连接到EF中,以提供精确的查询时间。
Include的问题在于,EF非常不擅长连接数据,因为它们使用连接数据而不是加载多个集合。我写了一篇关于此的博客文章,其中包括数字和可能的解决方法。
但总结我的研究结果是,联接策略有多糟糕取决于数据的形状。如果您将表A中的一行与表B中的一行进行连接,就像您在这里的情况,那么就没有问题。当您加载少量实体或所加载的实体之一非常小的时候,也几乎不会注意到它。
在您的情况下,由于您只寻找顶部帖子,因此我想看看的唯一优化是投影数据,以便您不加载可能不需要的属性。但最有可能的是,您所做的任何事情都只会花费几微秒。
尽管如此,我从Twitter的对话中得知这是只读场景。这使得可以向查询中添加AsNoTracking(),从而减少dbcontext的工作量(这是应用程序服务器上CPU和内存的改进,而不是数据库)。
所以,进行测量。我希望在数据库内运行时间小于1毫秒,加上一些传输时间,那么就没有太多值得改进的了。可能最好添加缓存。
更新:再次仔细阅读后,我意识到我的头脑产生了错误的查询计划,你可以通过将include移动到最后一个.SelectMany(x => x.Posts)之后,并将其更改为.Include(post => post.User),让你只加载帖子和用户,而不是类别和主题。它仍然会连接它们,但不会加载数据。
更新2:写Query1的示例。我不确定是否有区别,但我认为它可能会减少加载的数据。您需要查看分析器。
这是一段查询语句,大致意思是:获取指定分类下最新的帖子,并包含发帖人的信息。具体操作包括:先获取指定分类,然后获取该分类下所有主题,再获取每个主题下的所有帖子,最后按照创建时间倒序排序并返回第一个结果。需要注意的是,是否能达到最优查询效果还需要检查生成的实际查询语句。

嗨Mikael,感谢您的评论。关于查询1怎么样?在您的博客文章中,您只涵盖了一级包含。但是如果我尝试按照您的建议加载所有集合并在内存中合并它们...这是否是您的方法? - YodasMyDad
1
当加载单个实体时,您确实不需要经历所有这些麻烦。无论如何,这将非常快速。请查看新的更新,了解我如何编写Q1。 - Mikael Eliasson

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