public class Person{
public int Id{get;set;}
[ConcurrentCheck]
public string FirstName{get;set;}
public int Age{get;set;}
}
ConcurrentCheck при параллельном изменении записи, если была открыта транзакция А, после этого транзакция B, в транзакции B была изменена колонка FirstName, то при при SaveChanges(отправке запроса в базу данных будет DbUpdateConcurrencyException:
using(var context = new Context(options)){
var person = context.Persons.First(x => x.Id == 1);//1
context.Database.ExecuteSqlCommand("UPDATE Persons SET FirstName = 'Jane' WHERE Id = 1;");//2
person.Age = 12;
context.SaveChanges(); // DbUpdateConcurrencyException
}
Concurrency Ошибка возникает потому, что в строке 1 мы получаем текущий экземляр Person, в строке два обновляем колонку FirstName, при этом person - это старый, необновленный, экземпляр. Ef Core видит, что у свойства FirstName есть соответсвующий атрибут и при вызове SaveChanges (16 строка) дополняет построенный sql:
Тем самым добавляя в фильтрацию Where условие, если FirstName изменено, то Sql Server просто не сможет выполнить update и вернет ошибку, лечится это таким образом:
using (var context = new Context(optionsBuilder.Options))
{
try
{
var person = context.Persons.First(x => x.Id == 1); //1
context.Database.ExecuteSqlCommand("UPDATE Persons SET FirstName = 'Jane' WHERE Id = 1;"); //2
person.Age = 12;
context.SaveChanges(); // DbUpdateConcurrencyException
}
catch (DbUpdateConcurrencyException exception)
{
var entries = exception.Entries;
foreach (var entry in entries)
{
// context пытается добавить эти значения в таблицу
var currentEntryProps = entry.CurrentValues;
// это реальные значения из базы данных
var dbProps = entry.GetDatabaseValues();
var dbPropsClone = dbProps.Clone();
foreach (var prop in dbPropsClone.Properties)
{
if (prop.Name == "Age")
dbProps["Age"] = currentEntryProps["Age"];
}
// OriginalValues - данные, которые context ожидает найти в таблице
entry.OriginalValues.SetValues(dbProps);
}
context.SaveChanges();
}
}
Если поменять местами строки 1 и 2, то ничего страшного не произойдет, т.к в строке 2 будет сделан запрос к базе данных и получен измененный экземпляр Person
В каких-то случаях имеет смысл использование транзакций, ef core предоставляет возможность транзакций для реляционных баз данных:
using (var context = new Context(optionsBuilder.Options))
{
using var transaction = context.Database.BeginTransaction();
try
{
var person = context.Persons.First();
context.Database.ExecuteSqlCommand("UPDATE Persons SET FirstName = 'Jane1' WHERE Id = 1;"); //2
person.Age = 1122;
context.SaveChanges();
transaction.Commit();
}
catch (DbUpdateConcurrencyException ex)
{
// не нужно
// transaction.Rollback();
}
}
До вызова Commit никакие изменения не будут применены к базе, sql server сам открывает соединение(неточно) и начинает строит запросы в рамках транзакции, при это любые вызовы SaveChanges или ExecuteSqlCommand будут проверяться на корректность (using выше упадет из-за изменения Concurrent Check поля), но приментся только при вызове transaction.Commit().Причем Rollback в catch вызывать не нужно, он просто очищает все транзакции, которые уже были проведены
Также есть еще одна методика обеспечения согласованного состояния, это использование RowVersion в MSQL Server'e. Колонка RowVersion увеличивается при добавлении каждой новой записи, тут принцип работы тот же самый, при Update и Delete сущности в SQL дописывается фильтрация по значению RowVersion полученному из базы в начале операции, если значение уже другое, то сущность, которую нужно (изменить/удалить) просто не найдется в базе и EF Core кинет ошибку:
Database operation expected to affect 1 row(s) but actually affected 0 row(s). Data may have been modified or deleted since entities were loaded. See http://go.microsoft.com/fwlink/?LinkId=52
7962 for information on understanding and handling optimistic concurrency exceptions.
Плюсы использования RowVersion это скорость выборки, если ставить ConcurrencyLock на большое количество полей, то в итоге запрос будет выполняться очень медленно, за RowVersion следит MSQL Server и выполняется дополнительная фильтрация только по одному столбцу