Entity Framework 导航 属性 preload/reuse

Entity Framework Navigation Property preload/reuse

当我期望可以从 EF 缓存中获取对象时,为什么 Entity Framework 正在执行查询?

有了这些简单的模型类:

public class Blog
{
    public int Id { get; set; }
    public string Name { get; set; }
    public virtual ICollection<Post> Posts { get; set; }
}

public class Post
{
    public int Id { get; set; }
    public string Content { get; set; }
    public virtual Blog Blog { get; set; }
}

public class BlogDbContext : DbContext
{
    public BlogDbContext() : base("BlogDbContext") {}

    public DbSet<Blog> Blogs { get; set; }
    public DbSet<Post> Posts { get; set; }
}

我分析了以下操作的查询

public class HomeController : Controller
{
    public ActionResult Index()
    {
        var ctx = new BlogDbContext();

        // expecting posts are retrieved and cached by EF
        var posts = ctx.Posts.ToList();
        var blogs = ctx.Blogs.ToList();

        var wholeContent = "";

        foreach (var blog in blogs)
            foreach (var post in blog.Posts) // <- query is executed
                wholeContent += post.Content;

        return Content(wholeContent);
    }
}

为什么 EF 不重新使用我已经通过 var posts = ctx.Posts.ToList(); 语句获取的 Post 实体?

进一步说明:

一个现有的应用程序有一个 Excel 导出报告。数据是通过一个主要的 Linq2Sql 查询和一个包含树(~20)来获取的。然后它通过自动映射器映射,并添加来自手动缓存的附加数据(如果添加到主查询,以前会减慢执行速度)。

现在数据增长了,SQL服务器在尝试执行错误查询时崩溃:

The query processor ran out of internal resources and could not produce a query plan.

延迟加载会导致超过 100.000 个查询。所以我想我可以通过几个简单的查询预加载所有需要的数据,并让 EF 在延迟加载期间自动使用缓存中的对象。

我最初在 TSQL IN() 子句的限制方面遇到了其他问题,我用 MoreLinq 的 Batch 扩展解决了这些问题。

启用延迟加载后,EF 仍会重新加载集合导航属性。可能是因为 EF 不知道你是否真的加载了所有 Post。 EG代码如

   var post = db.Posts.First();
   var relatedPosts = post.Blog.Posts.ToList();

会很棘手,因为博客已经加载了一个 Post,但显然需要获取其他的。

在任何情况下,当依靠更改跟踪器来修复您的导航属性时,您都应该禁用延迟加载。 EG

using (var db = new BlogDbContext())
{
    db.Configuration.LazyLoadingEnabled = false;
    . . .

鉴于您拥有导航属性,请考虑在您的查询中利用它们来为 Automapper 提供一个动态对象以映射到您的 ViewModel/DTO 而不是您将依赖预加载的顶级实体或等待延迟加载。

这是通过对您的查询发出 .Select() 来完成的。使用一个简单的示例来提取订单详细信息,包括客户名称、订单行中的产品名称和数量列表,以及订单引用客户的交货地址,并且该客户有一个交货地址,订单行的集合, 每个都有一个产品...

var orderDetails = dbContext.Orders
.Where(o => /* Insert criteria */)
.Select(o => new 
{
   o.OrderId,
   o.OrderNumber,
   o.Customer.CustomerId,
   CustomerName = x.Customer.FullName,
   o.Customer.DeliveryAddress, // Address entity if no further dependencies, or extract fields/relations from the Address.
   o.OrderLines.Select( ol = > new 
   {
      ol.OrderLineId,
      ProductName = ol.Product.Name,
      ol.Quantity
   }
}).ToList(); // Ready to feed into Automapper.

有了 ~20,您的 Select 无疑会涉及更多,但我们的想法是向 SQL 服务器提供一个查询以检索您想要的数据,然后您可以将其输入Automapper 导航任何子关系可以被 EF 展平或简化的位置,并 returned 以便您的映射器充实到生成的模型中。

随着系统的增长,您还需要考虑利用分页 /w Skip and Take 而不是 ToList,或者至少利用 Take 来确保 return 的数据量有上限。 ToList 是我在 EF 代码中寻找的主要性能巨魔,因为它的误用会杀死应用程序。