Я читал другие вопросы о том, как реализовать семантику if-exists-insert-else-update в EF, но я не понимаю, как работают ответы, или они на самом деле не решают проблему. Общим решением является перенос работы в области транзакции (например: Внедрение if-not-exist-insert с использованием 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();
}
}
Но я не вижу, как это решает что угодно, так как для этого нужно работать, строка, которую я снял выше, должна блокироваться, если второй поток пытается получить доступ к одному и тому же идентификатору пользователя, разблокируя только тогда, когда первый поток завершил свою работу. Однако использование транзакции не приведет к этому, и мы получим исключение UpdateException, вызванное нарушением ключа, которое возникает, когда второй поток пытается создать того же пользователя во второй раз.
Вместо того, чтобы поймать исключение, вызванное состоянием гонки, было бы лучше предотвратить состояние гонки в первую очередь. Одним из способов сделать это было бы для выделенной строки вынести исключительную блокировку в строке базы данных, которая соответствует ее условию, что означает, что в контексте этого блока только один поток за раз мог работать с пользователем.
Кажется, что это должна быть распространенная проблема для пользователей EF, поэтому я ищу чистое универсальное решение, которое я могу использовать везде.
Мне бы очень хотелось избежать использования хранимой процедуры для создания моего пользователя, если это возможно.
Любые идеи?
EDIT. Я попытался выполнить вышеуказанный код одновременно на двух разных потоках, используя один и тот же идентификатор пользователя, и, несмотря на то, что вы делали сериализуемые транзакции, они оба могли одновременно входить в критический раздел (*). Это приводит к выходу UpdateException, когда второй поток попытался вставить тот же идентификатор пользователя, который только что вставил. Это связано с тем, что, как указывает Ладислав ниже, сериализуемая транзакция принимает исключительные блокировки только после того, как она начала изменять данные, а не читать.