EF 4.3.1和EF 5.0 DbSet.Local 比实际数据库查询慢

21

我有一个包含大约16,500个城市的表格数据库,以及相应的EF数据模型(基于数据库)。我使用以下代码将其预加载到内存中:

Db.Cities.Load()

...然后在使用它们的时候,我尝试了以下每个查询:

Dim cities() As String = Db.Cities.Select(Function(c) c.CityName).ToArray

Dim cities() As String = Db.Cities.Local.Select(Function(c) c.CityName).ToArray

第一个查询很快(约10毫秒),但第二个查询第一次运行需要约2.3秒的时间(尽管在之后的调用中比第一个查询更快)。

这没有意义,因为SQL Server Profiler验证了第一个查询正在另一台机器上访问数据库,但第二个查询没有!

我已经尝试关闭Db.Configuration.AutoDetectChangesEnabled,并尝试预生成视图。

我应该怎么做才能使.Local更快?(并非所有运行此应用程序的客户端都在快速局域网上。)


我应该注意到,我正在使用的是.NET 4.0而不是4.5,因此对“EF 5.0”的测试实际上是针对EF 5.0的.NET 4.0版本(也称为EF 4.4.0)。不幸的是,目前这个项目不能选择.NET 4.5。 - MCattle
3个回答

9
我使用Resharper方便的功能查看了Local属性的源代码。你会首先看到一个对DetectChanges的调用,如果你只运行以上三行代码,这可能不是你的问题。但是EF会为Local创建一个新的ObservableCollection,并逐个填充它们。在第一次调用时,这两者都可能代价高昂。
直接对DbSet进行查询将路由到EF数据库提供程序,我确信它们会直接访问内部本地缓存。

点赞,因为指导了我正确的方向;我不知道 ReSharper 允许我深入挖掘 EF 的内部。 - MCattle
Resharper是一个非常好的工具。你也会在其中看到EF跟踪LocalObservableCollection。我强烈建议除了只读目的之外,不要访问内部缓存。但我一次又一次地发现,在预加载时将缓存存储在数组或列表中(就像@Akash所说的那样)确实是正确的方法。有很多方法可以做到这一点,而不会增加太多的语法开销。 - N Jones

6
下面这个扩展方法将返回一个包含DbSet本地缓存实体的IEnumerable,避免了使用DbSet.Local()方法检测上下文更改和创建ObservableCollection对象所带来的启动开销。
<Extension()>
Public Function QuickLocal(Of T As Class)(ByRef DbCollection As DbSet(Of T)) As IEnumerable(Of T)
    Dim baseType = DbCollection.[GetType]().GetGenericArguments(0)
    Dim internalSet = DbCollection.GetType().GetField("_internalSet", Reflection.BindingFlags.NonPublic Or Reflection.BindingFlags.Instance).GetValue(DbCollection)
    Dim internalContext = internalSet.GetType().GetProperty("InternalContext").GetValue(internalSet, Nothing)
    Return DirectCast(internalContext.GetType.GetMethod("GetLocalEntities").MakeGenericMethod(baseType).Invoke(internalContext, Nothing), IEnumerable(Of T))
End Function

在一个包含19,679个实体的DbSet上调用.QuickLocal只需要9毫秒,而在第一次调用时调用.Local需要2121毫秒。

4
如果这是问题的原因,你可以避免反射并直接访问 ObjectStateManager((IObjectContextAdapter)dbContext).ObjectContext.ObjectStateManager.GetObjectStateEntries(EntityState.Unchanged | EntityState.Added | EntityState.Modified).Select(entry => entry.Entity).OfType<T>() - user743382
这更简单,但是当在DbSet上编写扩展方法以将Local()替换为QuickLocal()时,无论如何都没有简便的方法来获取DbSet对象的DbContext,仍需使用反射方式。 - MCattle
你说得对,你可以使用它将Db.Cities.Local更改为Db.GetLocalEntities<City>(),但是如果你将Db.Cities传递给一个方法,你就会被卡住,需要使用反射。 - user743382
实际上,在EF源代码http://entityframework.codeplex.com/SourceControl/latest#src/EntityFramework/Internal/InternalContext.cs中可以看到,GetLocalEntities只是使用了hvd的代码(实际上比hvd的OfType想法更长,因为它从EF Where().Select()中选择了更好的方式)。 - yoel halb

0
为什么不直接保存第一个查询的字符串列表并使用它呢?
List<string> cities = db.Cities.Select( x=>x.CityName).ToList();

由于 Select 可能会执行一些一致性检查,因此本地可能会变慢。


这是我倾向的方向,但目前我正在使用循环来调用所有DbSets上的.Load()。应用程序中有大约77个表加载到内存中以便快速引用。我怀疑第一次调用.Local时创建了ObservableCollection<T>(带有一致性检查),但实际上我们并没有使用ObservableCollection<T>的功能。如果我找不到更快获取本地数据的方法,那么我将把表加载到List<T>集合中。 - MCattle
将其作为已接受的解决方案,因为依赖内部缓存会引起其他问题,比如能否使用来自数据库的新值更新缓存对象。我通过编写 T4 模板来处理创建、填充和管理大量 List<T> 引用表的开销,因此我只需要根据需要修改 .edmx 文件。 - MCattle

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