EF 4.3.1 и EF 5.0 DbSet.Local медленнее, чем фактический запрос к базе данных.

У меня есть база данных с таблицей примерно из 16 500 городов и модель данных EF (Database-First) для этой базы данных. Я предварительно загружаю их в память с помощью кода:

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 работал быстрее? (Не все клиенты, работающие с этим приложением, будут находиться в быстрой локальной сети.)


person MCattle    schedule 31.08.2012    source источник
comment
Я должен отметить, что я использую .NET 4.0, а не 4.5, поэтому тест на EF 5.0 фактически проводится в версии EF 5.0 для .NET 4.0 (он же EF 4.4.0). К сожалению, на данный момент .NET 4.5 не подходит для этого проекта.   -  person MCattle    schedule 01.09.2012


Ответы (3)


Почему бы вам просто не сохранить список строк из первого запроса и использовать его вместо этого.

List<string> cities = db.Cities.Select( x=>x.CityName).ToList();

Local может быть медленнее из-за Select, который может выполнять некоторые проверки согласованности.

person Akash Kava    schedule 03.09.2012
comment
Это то, к чему я склоняюсь, но в настоящее время я использую цикл для вызова .Load() во всех DbSet. В приложении в память загружено около 77 таблиц для быстрого ознакомления. Я подозреваю, что первый вызов .Local создает ObservableCollection‹T› (с проверкой согласованности), но на самом деле мы не используем функции ObservableCollection‹T›. Если я не найду более быстрого способа получить локальные данные, я загружу таблицы в коллекции List‹T›. - person MCattle; 03.09.2012
comment
Сделать это общепринятым решением, поскольку использование внутреннего кеша вызывает другие проблемы, такие как возможность обновлять кэшированные объекты новыми значениями из базы данных. Я справился с накладными расходами на создание, заполнение и управление большим количеством справочных таблиц List‹T›, написав шаблон T4 для выполнения тяжелой работы, поэтому мне нужно только изменить файл .edmx по мере необходимости. - person MCattle; 12.09.2012

Я просмотрел исходный код свойства Local, используя удобную функцию Resharper. Сначала вы увидите вызов DetectChanges, который, вероятно, не является вашей проблемой, если все, что вы используете, это три строки выше. Но затем EF создает новую коллекцию ObservableCollection для Local и заполняет ее поэлементно. Любой из них может быть дорогостоящим при первом звонке.

Запрос непосредственно к DbSet будет направлен поставщикам базы данных EF, которые, я уверен, имеют прямой доступ к внутреннему локальному кешу.

person N Jones    schedule 05.09.2012
comment
Проголосовал за то, что указал мне правильное направление; Я не знал, что ReSharper позволил мне копаться во внутренностях EF. - person MCattle; 05.09.2012
comment
Resharper — отличный инструмент для этого. Вы также увидите там, что EF отслеживает Local ObservableCollection. Я бы настоятельно рекомендовал не обращаться к внутреннему кешу, кроме как только для чтения. Но я снова и снова обнаруживал, что кэширование в массивах или списках, когда вы выполняете предварительную загрузку (как говорит @Akash), - это действительно правильный путь. Есть много способов сделать это, не добавляя слишком много синтаксических издержек. - person N Jones; 06.09.2012

Следующий метод расширения вернет IEnumerable<T>, содержащий локальные кэшированные сущности DbSet, без дополнительных затрат на запуск, связанных с обнаружением изменений контекста методом DbSet.Local() и созданием объекта ObservableCollection<T>.

<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

Вызов .QuickLocal для DbSet, содержащего 19 679 сущностей, занимает 9 мс, тогда как вызов .Local занимает 2121 мс при первом вызове.

person MCattle    schedule 05.09.2012
comment
Ах, если это проблема, вы можете избежать размышлений и получить прямой доступ к ObjectStateManager: ((IObjectContextAdapter)dbContext).ObjectContext.ObjectStateManager.GetObjectStateEntries(EntityState.Unchanged | EntityState.Added | EntityState.Modified).Select(entry => entry.Entity).OfType<T>() - person ; 05.09.2012
comment
Это проще, но при написании метода расширения для DbSet для замены Local() на QuickLocal() нет простого способа получить DbContext объекта DbSet без использования отражения. - person MCattle; 05.09.2012
comment
Вы правы, вы можете использовать его, чтобы изменить Db.Cities.Local на Db.GetLocalEntities<City>(), но если вы передадите Db.Cities методу, вы застряли, и вам нужно подумать. - person ; 05.09.2012
comment
На самом деле см. исходный код EF entityframework.codeplex.com/SourceControl. /latest#src/, что GetLocalEntities просто использует код hvd (на самом деле немного дольше, так как идея hvd OfType лучше из EF Where().Select()) - person yoel halb; 04.12.2013