Skip to content

Instantly share code, notes, and snippets.

@eterekhin
Last active June 20, 2019 05:59
Show Gist options
  • Save eterekhin/ee1ad5877ee37143da79b4bcbadaee16 to your computer and use it in GitHub Desktop.
Save eterekhin/ee1ad5877ee37143da79b4bcbadaee16 to your computer and use it in GitHub Desktop.
Ef Core Transactions

EF Core Concurrency

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 и выполняется дополнительная фильтрация только по одному столбцу

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment