Дополнительные статьи

Параллелизм

Последнее обновление: 22.11.2021

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

Для решения проблемы параллелизма Entity Framework Core предлагает использовать токены параллелизма или concurrency token.

Если свойство установлено в качестве concurrency token (токен параллелизма), Entity Framework перед сохранением изменений будет проверять его значение. Если значение отличается, то какой-то другой пользователь уже произвел изменения над данными. В этом случае EF Core применяет принцип оптимистичного параллелизма (optimistic concurrency), при котором, если сохраняемые данные уже были кем-то изменены, то выбрасывается ошибка.

Атрибут ConcurrencyCheck

Атрибут ConcurrencyCheck позволяет решить проблему параллелизма, когда с одной и той же записью в таблице могут работать одновременно несколько пользователей. Например:

using System.ComponentModel.DataAnnotations;
using Microsoft.EntityFrameworkCore;

public class User
{
    public int Id { get; set; }
    [ConcurrencyCheck]
    public string? Name { get; set; }
}
public class ApplicationContext : DbContext
{
    public DbSet<User> Users { get; set; } = null!;

    public ApplicationContext()
    {
        Database.EnsureCreated();
    }
    protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder)
    {
        optionsBuilder.UseSqlite("Data Source=helloappc.db");
    }
    protected override void OnModelCreating(ModelBuilder modelBuilder)
    {
        modelBuilder.Entity<User>().HasData(new User { Id = 1, Name = "Tom"});
    }
}

В обычном режиме Entity Framework Core при обновлении смотрит на Id и если Id записи в таблице совпадает с Id в передаваемой модели User, то строка в таблице обновляется. При использовании атрибута ConcurrencyCheck EF смотрит не только на Id, но и на исходное значение свойства Name. И если оно совпадает с тем, что имеется в таблице, то запись обновляется. Если же не совпадает (то есть кто-то уже успел обновить), то EF генерирует исключение DbUpdateConcurrencyException:

using Microsoft.EntityFrameworkCore;

using (ApplicationContext db = new ApplicationContext())
{
    try
    {
        User? user = db.Users.FirstOrDefault();
        if (user != null)
        {
            user.Name = "Bob";
            db.SaveChanges();
            Console.WriteLine(user.Name);
        }
    }
    catch (DbUpdateConcurrencyException ex)
    {
        Console.WriteLine(ex.Message);
    }
}

Метод IsConcurrencyToken

В Fluent API токен параллелизма настраивается с помощью метода IsConcurrencyToken():

using System.ComponentModel.DataAnnotations;
using Microsoft.EntityFrameworkCore;

public class User
{
    public int Id { get; set; }
    public string? Name { get; set; }
}
public class ApplicationContext : DbContext
{
    public DbSet<User> Users { get; set; } = null!;

    public ApplicationContext()
    {
        Database.EnsureCreated();
    }
    protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder)
    {
        optionsBuilder.UseSqlite("Data Source=helloappc.db");
    }
    protected override void OnModelCreating(ModelBuilder modelBuilder)
    {
        modelBuilder.Entity<User>().Property(b => b.Name).IsConcurrencyToken();
    }
}

Атрибут Timestamp

Другой механизм по отслеживанию изменений объекта в БД представляет атрибут Timestamp.

using System.ComponentModel.DataAnnotations;
using Microsoft.EntityFrameworkCore;

public class User
{
    public int Id { get; set; }
    public string? Name { get; set; }

    [Timestamp]
    public byte[]? Timestamp { get; set; }
}
public class ApplicationContext : DbContext
{
    public DbSet<User> Users { get; set; } = null!;

    public ApplicationContext()
    {
        Database.EnsureCreated();
    }
    protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder)
    {
        optionsBuilder.UseSqlServer(@"Server=(localdb)\mssqllocaldb;Database=helloappdb2;Trusted_Connection=True;");
    }
    protected override void OnModelCreating(ModelBuilder modelBuilder)
    {
        modelBuilder.Entity<User>().HasData(new User { Id = 1, Name = "Tom" });
    }
}

Атрибут Timestamp указывает, что значение свойства Timestamp будет включаться в создаваемое Entity Frameworkом SQL-выражение WHERE при отправке в базу данных команд на обновление и удаление. То есть значение данного свойства генерируется при добавлении объекта, а также каждый раз, когда пользователь будет обновлять его. В качестве типа для свойства используется массив байтов.

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

Стоит отметить, что реализация этой функциональности зависит от провайдера БД. Так, для SQLite это не окажет никакого влияния, поэтому в примере выше подключение идет к MS SQL Server.

Fluent API и метод IsRowVersion

Вместо атрибута Timestamp можно во Fluent API использовать метод IsRowVersion, который установит, что столбец представляет версию строки. Но опять же работа данной функциональности зависит от конкретного провайдера бд, например, для MS SQL Server эта функциональность работает:

using Microsoft.EntityFrameworkCore;

public class User
{
    public int Id { get; set; }
    public string? Name { get; set; }
    public byte[]? Timestamp { get; set; }
}
public class ApplicationContext : DbContext
{
    public DbSet<User> Users { get; set; } = null!;

    public ApplicationContext()
    {
        Database.EnsureCreated();
    }
    protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder)
    {
        optionsBuilder.UseSqlServer(@"Server=(localdb)\mssqllocaldb;Database=helloappdb2;Trusted_Connection=True;");
    }
    protected override void OnModelCreating(ModelBuilder modelBuilder)
    {
        modelBuilder.Entity<User>().Property(b => b.Timestamp).IsRowVersion();
    }
}
Помощь сайту
Юмани:
410011174743222
Перевод на карту
Номер карты:
4048415020898850