Внедрение if-not-exist-insert с использованием Entity Framework без условий гонки
Используя LINQ-to-Entities 4.0, есть ли правильный шаблон или конструкция для безопасной реализации "if not exists then insert"?
Например, в настоящее время у меня есть таблица, которая отслеживает "избранное пользователя" - пользователи могут добавлять или удалять статьи из своего списка избранных.
Базовая таблица не является истинным отношением "многие ко многим", но вместо этого отслеживает некоторую дополнительную информацию, такую как дата добавления фаворита.
CREATE TABLE UserFavorite
(
FavoriteId int not null identity(1,1) primary key,
UserId int not null,
ArticleId int not null
);
CREATE UNIQUE INDEX IX_UserFavorite_1 ON UserFavorite (UserId, ArticleId);
Вставка двух избранных с одинаковой парой User/Article приводит к ошибке повторяющегося ключа по желанию.
В настоящее время я реализовал логику "if not exists then insert" в слое данных с помощью С#:
if (!entities.FavoriteArticles.Any(
f => f.UserId == userId &&
f.ArticleId == articleId))
{
FavoriteArticle favorite = new FavoriteArticle();
favorite.UserId = userId;
favorite.ArticleId = articleId;
favorite.DateAdded = DateTime.Now;
Entities.AddToFavoriteArticles(favorite);
Entities.SaveChanges();
}
Проблема с этой реализацией заключается в том, что она подвержена условиям гонки. Например, если пользователь дважды щелкает ссылку "добавить в избранное", на сервер могут быть отправлены два запроса. Первый запрос завершается успешно, в то время как второй запрос (тот, который видит пользователь) терпит неудачу с UpdateException, обертывающим исключение SqlException для ошибки повторяющегося ключа.
С помощью хранимых процедур T-SQL я могу использовать транзакции с подсказками блокировки, чтобы гарантировать, что состояние гонки никогда не произойдет. Есть ли чистый метод для предотвращения состояния гонки в Entity Framework без использования хранимых процедур или слепо глотающих исключений?
Ответы
Ответ 1
Вы можете попробовать обернуть его в транзакцию в сочетании с "известным" шаблоном try/catch:
using (var scope = new TransactionScope())
try
{
//...do your thing...
scope.Complete();
}
catch (UpdateException ex)
{
// here the second request ends up...
}
Ответ 2
Вы также можете написать хранимую процедуру, которая использует некоторые новые трюки из sql 2005 +
Используйте свой объединенный уникальный идентификатор (userID + articleID) в инструкции по обновлению, а затем используйте функцию @@RowCount, чтобы узнать, подсчитывает ли число строк > 0, если оно 1 (или больше), обновление обнаружило строку, соответствующую вашему идентификатору пользователя и ArticleID, если это 0, тогда вам все понятно, что нужно вставить.
например.
Обновить tablex set userID = @UserID, ArticleID = @ArticleID (у вас может быть больше свойств здесь, если в нем содержится объединенный уникальный идентификатор), где userID = @UserID и ArticleID = @ArticleID
if (@@RowCount = 0)
Начать
Вставить в таблицу...
Конец
Лучше всего, все это делается за один звонок, поэтому вам не нужно сначала сравнивать данные, а затем определять, следует ли вставлять их. И, конечно же, он остановит любые вставки и не вызовет ошибок (изящно?)