.NET Entity Framework и транзакции

Будучи новичком в Entity Framework, я действительно зациклен на том, как продолжить этот набор проблем. В проекте, над которым я сейчас работаю, весь сайт сильно интегрирован с моделью EF. Сначала доступ к контексту EF контролировался с помощью загрузочной машины Dependency Injection. По оперативным причинам мы не смогли использовать библиотеку DI. Я удалил это и использовал модель отдельных экземпляров контекстного объекта, где это необходимо. Я начал получать следующее исключение:

Тип "XXX" отображается более одного раза.

Мы пришли к выводу, что различные экземпляры контекста вызывают эту проблему. Затем я абстрагировал объект контекста в один статический экземпляр, к которому обращался каждый поток/страница. Теперь я получаю одно из нескольких исключений в отношении транзакций:

Новая транзакция не допускается, так как существуют другие потоки в сеансе.

Операция транзакции не может быть выполнена, поскольку есть ожидающие запросы, работающие над этой транзакцией.

ExecuteReader требует, чтобы команда имела транзакцию, когда соединение, назначенное команде, находится в ожидающей локальной транзакции. Свойство Transaction команды не было инициализировано.

Последнее из этих исключений произошло при операции загрузки. Я не пытался сохранить состояние контекста обратно в Db в потоке, который не удался. Однако был другой поток, выполняющий такую ​​операцию.

Эти исключения в лучшем случае являются прерывистыми, но мне удалось заставить сайт перейти в состояние, когда новые соединения были отклонены из-за блокировки транзакции. К сожалению, я не могу найти детали исключения.

Я предполагаю, что мой первый вопрос заключается в том, должна ли использоваться модель EF из статического единственного экземпляра? Кроме того, можно ли устранить необходимость в транзакциях в EF? Я попытался использовать объект TransactionScope без успеха...

Честно говоря, я много застрял здесь и не могу понять, почему (что должно быть) довольно простые операции вызывают такую ​​проблему...

Ответ 1

Создание одного глобального Entity Framework DbContext в веб-приложении очень плохо. Класс DbContext не является потокобезопасным (и то же самое относится и к классу ObjectContext Entity Framework v1). Он построен вокруг концепции единицы работы, и это означает, что вы используете ее для управления одним вариантом использования: таким образом, для бизнес-транзакции. Он предназначен для обработки одного запроса.

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

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

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

Решением вашей проблемы является создание как минимум одного DbContext каждого запроса. Хотя теоретически вы можете кэшировать контекст объекта в пользовательском сеансе, это тоже плохая идея, потому что в этом случае DbContext обычно будет жить слишком долго и будет содержать устаревшие данные (поскольку его внутренний кэш не будет автоматически обновляться).

Также обратите внимание, что наличие одного DbContext на поток так же плохо, как наличие одного экземпляра для всего веб-приложения. ASP.NET использует пул потоков, что означает, что в течение жизненного цикла веб-приложения будет создано ограниченное количество потоков. По сути, это означает, что эти экземпляры DbContext в этом случае будут существовать в течение всего жизненного цикла приложения, вызывая те же проблемы со стабильностью данных.

Вы можете подумать, что наличие одного DbContext каждого потока на самом деле является потокобезопасным, но обычно это не так, поскольку ASP.NET имеет асинхронную модель, которая позволяет завершать запросы в другом потоке, отличном от того, где он был запущен (и в последних версиях MVC и Web API даже позволяют произвольному количеству потоков обрабатывать один запрос в последовательном порядке). Это означает, что поток, который запустил запрос и создал ObjectContext может стать доступным для обработки другого запроса задолго до того, как этот начальный запрос завершится. Однако объекты, используемые в этом запросе (например, веб-страница, контроллер или любой бизнес-класс), могут по-прежнему ссылаться на этот DbContext. Поскольку новый веб-запрос выполняется в том же потоке, он получит тот же экземпляр DbContext что и старый запрос. Это снова вызывает состояние гонки в вашем приложении и вызывает те же проблемы безопасности потоков, что и один глобальный экземпляр DbContext.

Ответ 2

Как вы ссылаетесь на "сайт" в своем вопросе, я предполагаю, что это веб-приложение. Статические члены существуют только один раз для всего приложения, если вы используете шаблон одноэлементного типа с единственным экземпляром контекста во всем приложении, все виды запросов будут находиться во всех видах состояний по всему приложению.

Один экземпляр статического контекста не будет работать, но несколько экземпляров контекста для потока будут неприятными, а также не могут смешивать контексты и сопоставлять их. Вам нужен только один контекст для каждого потока. Мы сделали это в нашем приложении, используя шаблон типа инъекции зависимостей. Наши классы BLL и DAL воспринимают контекст как параметр в методах, таким образом вы можете сделать что-то вроде ниже:

using (TransactionScope ts = new TransactionScope())
{
    using (ObjectContext oContext = new ObjectContext("MyConnection"))
    {
        oBLLClass.Update(oEntity, oContext);
    }
}

Если вам нужно вызвать другие методы BLL/DAL в вашем обновлении (или какой бы метод вы ни выбрали), вы просто передаете один и тот же контекст. Таким образом, обновления/вставки/удаления являются атомарными, eveything внутри одного метода использует один и тот же экземпляр контекста, но этот экземпляр не используется другими потоками.