我们说我有以下几类:
public class MegaBookCorporation
{
public int ID { get; private set}
public int BooksInStock
{
get
{
return Stores.Sum( x => x.BooksInStock)
}
}
public virtual ICollection<MegaBookCorporationStore> Stores { get; set; }
}
public class MegaBookCorporationStore
{
public int ID { get; private set; }
public string BookStoreName { get; private get; }
public virtual MegaBookCorporation ManagingCorporation { get; private set;}
public int BooksInStock
{
get
{
return Books.Where( x=> !x.IsSold).Count();
}
}
public virtual ICollection<Book> Books { get; set; }
}
public class Book
{
public int IndividualBookTrackerID { get; private set; }
public virtual MegaBookCorporationStore { get; private set; }
public bool IsSold { get; private set; }
public DateTime? SellingDate { get; private set;}
}
我在工作中讨论了在MegaBookCorporation中检索NumberOfBooks时遇到的性能问题。两个重要的事实:
1 /我们按照虚拟关键字的建议使用带延迟加载的EF 6。
2 /由于每本书都是单独跟踪的,因此数据库中的书籍条目数量将很快变得很快。从长远来看,该表的规模可能会达到数亿。我们每天可能会增加100,000本书。
我支持的意见是目前的实施情况很好,而且我们不会遇到问题。我的理解是,在调用GetEnumerator时,将生成一个SQL语句来过滤集合。
我的同事提出的另一个建议是缓存书籍数量。这意味着更新字段&#34; int ComputedNumberOfBooks&#34;每当调用AddBookToStock()或SellBook()方法时。需要在Store和Corporation类中重复和更新此字段。 (当然我们需要注意并发)
我知道添加这些字段并不是什么大问题,但我对这个想法感到非常不满。对我来说,它似乎预先设计了一个不存在的问题,并且在我看来并不存在。
我决定再次检查我的索赔,发现了两个相互矛盾的答案:
One saying that the whole Books collection would be pulled to memory,因为ICollection只继承自IEnumerable。 The other saying the opposite : the navigation property will be treated as an IQueryable until it is evaluated。(为什么不,因为该属性由代理包装)
所以这是我的问题:
1-什么是真相?
2-即使引用了整个集合,也不要认为它不是什么大问题,因为它是IEnumerable(内存使用率低)。
3-您如何看待此示例中的内存消耗/性能,以及最佳方法是什么?
谢谢
答案 0 :(得分:1)
真相是什么?
如果您使用MegaBookCorporation.BooksInStock
来获取存储的图书总数,则将从数据库中加载所有图书。除了获取所有数据并在内存中进行评估之外,查询提供程序无法为属性getter的主体生成SQL表达式。
即使引用了整个集合,也不要认为它不是什么大问题,因为它是IEnumerable(内存使用率低)。
是的,这是一个大问题,因为它根本没有扩展。它与IEnumerable
的事实无关。问题是在评估Count()
之前获取所有数据。
您如何看待此示例中的内存消耗/性能,以及最佳方法是什么?
内存消耗将随着数据库中存储的书籍数量而增长。既然你只想得到他们的数量,那显然是不行的。 Here你可以看到如何正确地做到这一点。
答案 1 :(得分:0)
事实是,通过您定义的属性,可以加载整个书籍集。这就是原因。
理想情况下,您希望能够
var numberOfBooks = context.MegaBookCorporations
.Where(m => m.ID == someId)
.Select(m => m.BooksInStock)
.Single();
如果EF能够将其转换为SQL,那么您的查询只返回一个整数并且不会将任何实体加载到内存中。
但不幸的是,EF无法做到这一点。它将抛出BooksInStock
没有SQL转换的异常。
要绕过这个例外,你可以做:
var numberOfBooks = context.MegaBookCorporations
.Where(m => m.ID == someId)
.Single()
.BooksInStock;
这极大地改变了事情。 Single()
将一个MegaBookCorporation
吸引到内存中。访问其BooksInStock
属性会触发延迟加载MegaBookCorporation.Stores
。随后,对于每个Store
,将加载完整的Books
个集合。最后,LINQ操作(x => !x.IsSold
,Count
,Sum
)将应用于内存中。
所以在这种情况下,the first link是正确的。延迟加载总是加载完整的集合。加载集合后,将不会再次加载它们。
但second link也是正确的:)。
只要您设法在一个可以转换为SQL的LINQ语句中执行所有操作,就会在数据库中评估导航属性和谓词,并且不会发生延迟加载。但是,您无法使用BooksInStock
属性。
实现这一目标的唯一方法是使用像
这样的LINQ语句var numberOfBooks = context.MegaBookCorporations
.Where(m => m.ID == someId)
.SelectMany(m => m.Stores)
.SelectMany(s => s.Books)
.Count();
这使用一个连接和COUNT
执行非常有效的查询,只返回计数。
不幸的是,你的关键假设......
在调用GetEnumerator时,将生成一个SQL语句来过滤集合。
不完全正确。生成SQL语句,但不包括过滤器。你提到的书籍数量会导致严重的性能和内存问题。
如果您经常需要这些计数,并且您不想一直单独查询它们,那么应该做些什么。您的同事的想法,数据库中的冗余ComputedNumberOfBooks
字段可能是一个解决方案,但我同意您的观点。
应该以(几乎)所有成本避免冗余。最糟糕的是,它始终需要客户端应用程序来保持双方同步。或数据库触发器。
但是谈论数据库......如果这些计数很重要且经常被查询,我会在BooksInStock
表中引入一个计算列MegaBookCorporationStore
。它的公式可以简单地计算商店中的书籍数量。然后,您可以将此计算列作为标记为DatabaseGeneratedOption.Computed
的属性添加到您的实体。没有冗余。