EF5 Особенности использования прокси отслеживания изменений

В прошлом посте я рассказал о различных типах данных, которые поддерживаются в EF, но какой же тип выбрать для использования. На мой взгляд, исходя из указанных данных, вытекает следующее:

  • Не стоит использовать наследование от EntityObject или IPOCO вообще. Нет никаких плюсов в их использовании.
  • Используйте POCO сущности и сделайте навигационные свойства виртуальными, чтобы можно было получить плюсы ленивой загрузки.
  • Если вы сериализуете свои сущности, тогда стоит подумать о том, чтобы выключить прокси и отказаться от ленивой загрузки, так как восстановление объектов может быть проблематичным.

Да, пока ни слова об использовании прокси отслеживания изменений. Для того, чтобы указать на случаи из использования следует рассмотреть их более детально с технической точки зрения, на плюсы и минусы которые возникают по сравнению с POCO объектами.

Плюсы использования прокси

Плюсы использования прокси отслеживания изменений достаточно просты:

  • Для отслеживаемых сущностей изменения фиксируются в тот же момент и обновление связей происходит так же в момент изменений, не дожидаясь  вызова метода DetectChanges().
  • Возможно увеличение производительности в некоторых сценариях

Минусы использования прокси

Ниже перечислены некоторые минусы использования прокси:

  • Правила, которым должны следовать ваши классы для включения механизма создания прокси отслеживания изменений, просты, но в то же время сильно могут ограничивать вас в реализации. Вот эти правила:
    • Класс должен быть публичным и не закрытым к изменениям (sealed).
    • Все свойства должны иметь публичные или защищенные виртуальные get и set.
    • Коллекции для навигации должны быть определены как ICollection<T>. Нельзя использовать IList<T>, List<T>, HashSet<T> и так далее.
    • Так как правила строгие очень легко упустить какой-либо момент и в результате вы не получите прокси отслеживания изменений. Например легко пропустить маркер виртуальности свойства или же сделать set внутренним методом.
    • В процессе выполнения, EF создает EntityCollection<T> который присваивается переменным с типом ICollection<T>. Вы не можете поменять это поведение, а так же любая коллекция установленная в эти свойства заранее будет уничтожена. Например, установка данных в конструкторе не сработает.
    • Комплексные типы работают только со «снимком» для определения изменений, а соответственно требуют использования DetectChanges(), если комплексный тип данных был изменен. Если вы не меняете значения в комплексных типах (что в целом хорошая практика) тогда они и не требуют вызова DetectChanges(), но по умолчанию EF предполагает что они могут измениться.
    • Сущности ведут себя очень по-разному когда они ведутся контекстом и когда нет, так как доступ к контексту решает многие проблемы.
    • Вы должны использовать DbSet.Create(), вместо оператора «new» для создания объектов, которые будут отслеживать свои изменения.
    • Возможно падение производительности в некоторых сценариях.

Производительность

Вы наверно уже заметили, что производительность может быть как плюсом, так и минусом использования прокси. Все зависит от того, как вы используете сущности.

Перед тем как окунуться в технические детали раскрывающие различные аспекты производительности, стоит сказать, что для большинства реальных приложений не будет никакой заметной разницы в работе со «снимком» данных или же с прокси. Если ваше приложение работает с сотнями и тысячами объектов, то все будет хорошо при использовании любого подхода, единственное что сравнение на основе «снимка» проще для работы.

Основной плюс производительности при работе с прокси будет в сценарии, когда у вас в приложении есть большое количество сущностей и лишь их малая доля подвергается изменениям в результате работы. Причина этого кроется в том, что при использовании «снимка» потребуется полное сканирование объектов и графов связей для выявления этих малых изменений. При использовании прокси каждое изменение будет тут же обнаружено без общего сканирования.

Теперь можно представить себе обратную ситуацию, несколько малых изменений произошли во всех объектах. В этом случае будут большие дополнительные расходы на запись и реакцию на каждое изменение. Дополнительный код для записи состояния, запуска событий и так далее очень скоро дадут заметный эффект замедления.

Рассмотрим простой класс:

Представим, что у нас 10 000 записей и каждая обновляется в таком духе:

На моем ноуте это заняло примерно 6,3 секунды. Модификация свойств на виртуальные включило использование прокси и повторный запуск показал время в 6,2 секунды. Небольшой прирост производительности при использовании прокси.

Сейчас представим себе, что приложение обновляет одно и то же свойство несколько раз до того, как будет вызван метод SaveChanges() – может быть мы пересчитываем какое-то свойство несколько раз или же срабатывал какой-либо счетчик.  Если обновление свойств как в коде выше запустить не один раз, а три раза для каждого экземпляра класса до вызова SaveChanges(), то это займет все те же 6,3 секунды для «снимка» и 6,6 секунд для прокси. До сих пор небольшая разница в производительности, но прокси стали работать медленнее.

В конце концов попробуем, что произойдет если приложение будет записывать данные в свойства, которые уже имеют данные, как это может произойти при работе с DTO или с чем-то похожим. Т.е. возьмем всё те же 10 000 записей проведем такую вот операцию:

После чего вызовем SaveChanges(). Использованием «снимка» весь процесс сохранения занял около 0,1 секунды. Это произошло, потому что ничего не поменялось и метод SaveChanges() ничего не записал в базу данных. А как себя поведет прокси? Тот же самый процесс занял 5,2 секунды! Почему? Потому что с использованием прокси свойство было помечено как измененное, независимо от того, что туда записалось. Можно предположить, что это вообще неправильное поведение. Причина же такого поведения в том, что это стандартное поведение EntityObject, и IPOCO интерфейсы были выделены из EntityObject, а прокси основаны на реализации IPOCO.

Все слишком нестабильно…

Да, действительно, все слишком нестабильно с производительностью и конкретные цифры производительности сильно меняются от применения. Достаточно легко прийти к сценариям, когда прокси будут работать значительно быстрее, нежели «снимок». Однако основная идея все же в том, что прокси не являются универсальным средством для работы с EF и повышением производительности, и не должны быть использованы в таком качестве.

Итого

Общий совет наверно будет таким: используйте РОСО сущности и по необходимости делайте виртуальными свойствами навигации для ленивой загрузки. Используйте прокси только в том случае когда вы четко осознали все сложности связанные с их реализацией и понимаете причины по которым вы хотите их использовать. Т.е. когда вы заоптимизировали свое приложение вдоль и поперек и обнаружили что использование «снимка» является узким местом всей системы работы с данными.

По мотивам статей Артура Викерса.

 

 

Hard’n’Heavy!

 

 

Violet Tape

Оставить комментарий