Параллелизм базы данных относится к ситуациям, когда несколько процессов или пользователей получают доступ к одним и тем же данным в базе данных или изменяют их одновременно. Контроль параллелизма относится к особым механизмам, используемым для обеспечения согласованности данных при наличии одновременных изменений.

EF Core реализует оптимистичный контроль параллелизма, что означает, что он позволяет нескольким процессам или пользователям вносить изменения независимо друг от друга без затрат на синхронизацию или блокировку. В идеальной ситуации эти изменения не будут мешать друг другу и, следовательно, смогут иметь успех. В худшем случае два или более процессов попытаются внести конфликтующие изменения, и только один из них должен преуспеть.

Как работает управление параллелизмом в EF Core

Свойства, настроенные как токены параллелизма, используются для реализации оптимистического управления параллелизмом: всякий раз, когда во время SaveChanges выполняется операция обновления или удаления, значение токена параллелизма в базе данных сравнивается с исходным значением, считываемым EF Core.

  • Если значения совпадают, операция может быть завершена.
  • Если значения не совпадают, EF Core предполагает, что конфликтующую операцию выполнил другой пользователь, и прерывает текущую транзакцию.

Ситуация, когда другой пользователь выполнил операцию, конфликтующую с текущей операцией, называется конфликтом параллелизма.

Поставщики баз данных отвечают за реализацию сравнения значений токенов параллелизма.

В реляционных базах данных EF Core включает проверку значения маркера параллелизма в предложении WHERE любых инструкций UPDATE или DELETE. После выполнения операторов EF Core считывает количество затронутых строк.

Если строки не затронуты, будет обнаружен конфликт параллелизма, и EF Core выдаст DbUpdateConcurrencyException.

Например, мы можем настроить LastName на Person как токен параллелизма. Тогда любая операция обновления над Person будет включать проверку параллелизма в предложении WHERE:

SQLCopy

UPDATE [Person] SET [FirstName] = @p1
WHERE [PersonId] = @p0 AND [LastName] = @p2;

Разрешение конфликтов параллелизма

Продолжая предыдущий пример, если один пользователь попытается сохранить некоторые изменения в Person, но другой пользователь уже изменил LastName, будет выдано исключение.

В этот момент приложение может просто сообщить пользователю, что обновление не удалось из-за конфликтующих изменений, и двигаться дальше. Но может быть желательно предложить пользователю убедиться, что эта запись по-прежнему представляет то же самое фактическое лицо, и повторить операцию.

Этот процесс является примером разрешения конфликта параллелизма.

Разрешение конфликта параллелизма включает в себя слияние ожидающих изменений из текущего DbContext со значениями в базе данных. Какие значения объединяются, зависит от приложения и может определяться пользовательским вводом.

Доступны три набора значений, помогающих разрешить конфликт параллелизма:

  • Текущие значения — это значения, которые приложение пыталось записать в базу данных.
  • Исходные значения — это значения, которые были первоначально извлечены из базы данных до внесения каких-либо изменений.
  • Значения базы данных — это значения, которые в настоящее время хранятся в базе данных.

Общий подход к разрешению конфликта параллелизма:

  1. Поймай DbUpdateConcurrencyException во время SaveChanges.
  2. Используйте DbUpdateConcurrencyException.Entries, чтобы подготовить новый набор изменений для затронутых объектов.
  3. Обновите исходные значения маркера параллелизма, чтобы отразить текущие значения в базе данных.
  4. Повторяйте процесс, пока не перестанут возникать конфликты.

В следующем примере Person.FirstName и Person.LastName настроены как токены параллелизма. В том месте, где вы включаете логику приложения, чтобы выбрать значение для сохранения, есть комментарий // TODO:.

using (var context = new PersonContext())
{
    // Fetch a person from database and change phone number
    var person = context.People.Single(p => p.PersonId == 1);
    person.PhoneNumber = "555-555-5555";
// Change the person's name in the database to simulate a concurrency conflict
    context.Database.ExecuteSqlCommand(
    "UPDATE dbo.People SET FirstName = 'Jane' WHERE PersonId = 1");
    var saved = false;
    while (!saved)
    {
        try
        {
            // Attempt to save changes to the database
            context.SaveChanges();
            saved = true;
        }
        catch (DbUpdateConcurrencyException ex)
        {
            foreach (var entry in ex.Entries)
            {
                if (entry.Entity is Person)
                {
                    var proposedValues = entry.CurrentValues;
                    var databaseValues = entry.GetDatabaseValues();
                foreach (var property in proposedValues.Properties)
                    {
                      var proposedValue = proposedValues[property];
                      var databaseValue = databaseValues[property];
          // TODO: decide which value should be written to database
          // proposedValues[property] = <value to be saved>;
                    }
         // Refresh original values to bypass next concurrency check
                    entry.OriginalValues.SetValues(databaseValues);
                }
                else
                {
throw new NotSupportedException("Don't know how to handleconcurrency conflicts for " + entry.Metadata.Name);
                }
            }
        }
    }
}