Я читал другие вопросы о том, как реализовать семантику if-существующие-insert-else-update в EF, но либо я не понимаю, как работают ответы, либо они фактически не решают проблему. Обычное предлагаемое решение - заключить работу в область транзакции (например: Реализация вставки «если не существует» с использованием Entity Framework без условий гонки ):
using (var scope = new TransactionScope()) // default isolation level is serializable
using(var context = new MyEntities())
{
var user = context.Users.SingleOrDefault(u => u.Id == userId); // *
if (user != null)
{
// update the user
user.property = newProperty;
context.SaveChanges();
}
else
{
user = new User
{
// etc
};
context.Users.AddObject(user);
context.SaveChanges();
}
}
Но я не вижу, как это что-то решает, так как для того, чтобы это работало, строка, которую я пометил выше, должна block , если второй поток пытается получить доступ к тому же идентификатору пользователя, разблокируя только когда первый Нить закончила свою работу. Однако использование транзакции не приведет к этому, и мы получим исключение UpdateException из-за нарушения ключа, которое возникает, когда второй поток пытается создать того же пользователя во второй раз.
Вместо того, чтобы поймать исключение, вызванное состоянием гонки, было бы лучше предотвратить возникновение состояния гонки в первую очередь. Одним из способов сделать это было бы то, чтобы помеченная строка сняла монопольную блокировку для строки базы данных, которая соответствует его условию, то есть в контексте этого блока только один поток одновременно мог работать с пользователем.
Похоже, это должно быть распространенной проблемой для пользователей EF, поэтому я ищу чистое, общее решение, которое можно использовать везде.
Я бы очень хотел избежать использования хранимой процедуры для создания моего пользователя, если это возможно.
Есть идеи?
РЕДАКТИРОВАТЬ : я пытался выполнить вышеупомянутый код одновременно в двух разных потоках, используя один и тот же идентификатор пользователя, и, несмотря на выполнение сериализуемых транзакций, они оба могли одновременно входить в критическую секцию (*). Это приводит к возникновению исключения UpdateException, когда второй поток пытается вставить тот же идентификатор пользователя, который только что был добавлен первым. Это связано с тем, что, как указал Ладислав ниже, сериализуемая транзакция получает эксклюзивные блокировки только после того, как она начала изменять данные, а не читать.