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

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

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

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

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

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

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

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

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

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

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

2 ответа

Решение

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

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

Пожалуйста, подумайте об этом, почему это не может работать. ObjectContext содержит локальный кэш сущностей в вашей базе данных. Это позволяет вам сделать кучу изменений и, наконец, отправить эти изменения в базу данных. При использовании одного статического ObjectContext с несколькими пользователями, звонящими SaveChanges на этом объекте, как он должен знать, что именно должно быть совершено, а что нет? Поскольку он не знает, он сохранит все изменения, но в этот момент другой пользователь все еще может вносить изменения. Когда вам повезет, EF или ваша база данных потерпит неудачу, потому что сущности находятся в недопустимом состоянии. Если вам не повезло, объекты в недопустимом состоянии успешно сохранены в базе данных, и через несколько недель вы можете обнаружить, что ваша база данных полна дерьма. Решением вашей проблемы является создание хотя бы одного ObjectContext по запросу. Хотя теоретически вы можете кэшировать контекст объекта в сеансе пользователя, это тоже плохая идея, потому что ObjectContext обычно будет жить слишком долго и будет содержать устаревшие данные (потому что его внутренний кэш не будет автоматически обновляться).

ОБНОВЛЕНИЕ:

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

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

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

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

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

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

Другие вопросы по тегам