Обновление сущности в приложении EF Core с помощью SQLite дает исключение DbUpdateConcurrencyException - PullRequest
0 голосов
/ 07 октября 2018

Я пытаюсь использовать оптимистическую проверку параллелизма в EF Core с SQLite.Простейший положительный сценарий (даже без самого параллелизма) дает мне Microsoft.EntityFrameworkCore.DbUpdateConcurrencyException: '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.

Сущность:

public class Blog
{
    public Guid Id { get; set; }
    public string Name { get; set; }
    public byte[] Timestamp { get; set; }
}

Контекст:

internal class Context : DbContext
{
    public DbSet<Blog> Blogs { get; set; }

    protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder)
    {
        optionsBuilder.UseSqlite(@"Data Source=D:\incoming\test.db");
        ///optionsBuilder.UseSqlServer(@"Server=(localdb)\mssqllocaldb;Database=Blogging;Trusted_Connection=True;");
    }

    protected override void OnModelCreating(ModelBuilder modelBuilder)
    {
        modelBuilder.Entity<Blog>()
            .HasKey(p => p.Id);

        modelBuilder.Entity<Blog>()
            .Property(p => p.Timestamp)
            .IsRowVersion()
            .HasDefaultValueSql("CURRENT_TIMESTAMP");
    }
}

Пример:

internal class Program
{
    public static void Main(string[] args)
    {
        var id = Guid.NewGuid();
        using (var db = new Context())
        {
            db.Database.EnsureDeleted();
            db.Database.EnsureCreated();
            db.Blogs.Add(new Blog { Id = id, Name = "1" });
            db.SaveChanges();
        }

        using (var db = new Context())
        {
            var existing = db.Blogs.Find(id);
            existing.Name = "2";
            db.SaveChanges(); // Exception thrown: 'Microsoft.EntityFrameworkCore.DbUpdateConcurrencyException'
        }

    }
}

Я подозреваю, что это как-то связано с типами данных между EF и SQLite.При ведении журнала мне выдается следующий запрос:

Executing DbCommand [Parameters=[@p1='2bcc42f5-5fd9-4cd6-b0a0-d1b843022a4b' (DbType = String), @p0='2' (Size = 1), @p2='0x323031382D31302D30372030393A34393A3331' (Size = 19) (DbType = String)], CommandType='Text', CommandTimeout='30']
UPDATE "Blogs" SET "Name" = @p0
WHERE "Id" = @p1 AND "Timestamp" = @p2;

Но типы столбцов BLOB для идентификатора и метки времени (SQLite не предоставляет типы столбцов UUID и timestamp):

enter image description here


В то же время, если я использую SQL Server (используйте закомментированную строку подключения + удалить .HasDefaultValueSql("CURRENT_TIMESTAMP")), образец работает правильно и обновляет отметку времени в БД.

Используемые пакеты:

<PackageReference Include="Microsoft.EntityFrameworkCore.Sqlite" Version="2.1.4" />
<PackageReference Include="Microsoft.EntityFrameworkCore.Sqlite.Core" Version="2.1.4" />

Я неправильно настроил модель для проверки параллелизма?Это сводит меня с ума, что я не могу заставить его работать с этим самым простым сценарием.


ОБНОВЛЕНИЕ: как я наконец заставил это работать.Здесь показана только идея, но, вероятно, она кому-нибудь поможет:

public class Blog
{
    public Guid Id { get; set; }
    public string Name { get; set; }
    public long Version { get; set; }
}

internal class Context : DbContext
{
    public DbSet<Blog> Blogs { get; set; }

    protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder)
    {
        optionsBuilder.UseSqlite(@"Data Source=D:\incoming\test.db");
    }

    protected override void OnModelCreating(ModelBuilder modelBuilder)
    {
        modelBuilder.Entity<Blog>()
            .HasKey(p => p.Id);

        modelBuilder.Entity<Blog>()
            .Property(p => p.Version)
            .IsConcurrencyToken();
    }
}

internal class Program
{
    public static void Main(string[] args)
    {
        var id = Guid.NewGuid();
        long ver;
        using (var db = new Context())
        {
            db.Database.EnsureDeleted();
            db.Database.EnsureCreated();
            var res = db.Blogs.Add(new Blog { Id = id, Name = "xxx", Version = DateTime.Now.Ticks});
            db.SaveChanges();
        }

        using (var db = new Context())
        {
            var existing = db.Blogs.Find(id);
            existing.Name = "yyy";
            existing.Version = DateTime.Now.Ticks;
            db.SaveChanges(); // success
        }

        using (var db = new Context())
        {
            var existing = db.Blogs.Find(id);
            existing.Name = "zzz";
            existing.Version = DateTime.Now.Ticks;
            db.SaveChanges(); // success
        }

        var t1 = Task.Run(() =>
        {
            using (var db = new Context())
            {
                var existing = db.Blogs.Find(id);
                existing.Name = "yyy";
                existing.Version = DateTime.Now.Ticks;
                db.SaveChanges();
            }
        });

        var t2 = Task.Run(() =>
        {
            using (var db = new Context())
            {
                var existing = db.Blogs.Find(id);
                existing.Name = "zzz";
                existing.Version = DateTime.Now.Ticks;
                db.SaveChanges();
            }
        });

        Task.WaitAll(t1, t2); // one of the tasks throws DbUpdateConcurrencyException
    }
}

Ответы [ 3 ]

0 голосов
/ 10 октября 2018

Похоже, что поставщик EF Core SQLite неправильно обрабатывает [TimeStamp] (или IsRowVersion()) помеченные byte[] свойства при привязке их к параметрам запроса SQL.Используется стандартное byte[] в hex string преобразование, которое в данном случае неприменимо - byte[] на самом деле равно a string.

Сначала рассмотрите возможность сообщения об этом своему трекеру.Затем, до тех пор, пока это не будет решено (если когда-либо), в качестве обходного пути вы можете использовать следующий пользовательский ValueConverter:

class SqliteTimestampConverter : ValueConverter<byte[], string>
{
    public SqliteTimestampConverter() : base(
        v => v == null ? null : ToDb(v),
        v => v == null ? null : FromDb(v))
    { }
    static byte[] FromDb(string v) =>
        v.Select(c => (byte)c).ToArray(); // Encoding.ASCII.GetString(v)
    static string ToDb(byte[] v) =>
        new string(v.Select(b => (char)b).ToArray()); // Encoding.ASCII.GetBytes(v))
}

К сожалению, нет способа заставить EF Core использоватьэто только для параметров, поэтому после присвоения ему .HasConversion(new SqliteTimestampConverter()), теперь тип БД считается string, поэтому вам нужно добавить .HasColumnType("BLOB").

Окончательное рабочее отображение:

    modelBuilder.Entity<Blog>()
        .Property(p => p.Timestamp)
        .IsRowVersion()
        .HasConversion(new SqliteTimestampConverter())
        .HasColumnType("BLOB")
        .HasDefaultValueSql("CURRENT_TIMESTAMP");

Вы можете избежать всего этого, добавив следующее пользовательское «соглашение» SQLite RowVersion в конце вашего OnModelCreating:

if (Database.IsSqlite())
{
    var timestampProperties = modelBuilder.Model
        .GetEntityTypes()
        .SelectMany(t => t.GetProperties())
        .Where(p => p.ClrType == typeof(byte[])
            && p.ValueGenerated == ValueGenerated.OnAddOrUpdate
            && p.IsConcurrencyToken);

    foreach (var property in timestampProperties)
    {
        property.SetValueConverter(new SqliteTimestampConverter());
        property.Relational().DefaultValueSql = "CURRENT_TIMESTAMP";
    }
}

, чтобы конфигурация вашего свойства могла быть урезана до

modelBuilder.Entity<Blog>()
    .Property(p => p.Timestamp)
    .IsRowVersion();

или полностью удален и заменен аннотацией данных

public class Blog
{
    public Guid Id { get; set; }
    public string Name { get; set; }
    [Timestamp]
    public byte[] Timestamp { get; set; }
}
0 голосов
/ 22 июня 2019

Вдохновленный этим потоком на GitHub и ответом Ивана, я написал этот код, чтобы на моем модульном тестировании имитировать параллелизм SQL Server.

var connection = new SqliteConnection("DataSource=:memory:");

var options = new DbContextOptionsBuilder<ActiveContext>()
               .UseSqlite(connection)
               .Options;

var ctx = new ActiveContext(options);

if (connection.State != System.Data.ConnectionState.Open)
{
    connection.Open();

    ctx.Database.EnsureCreated();

    var tables = ctx.Model.GetEntityTypes();

    foreach (var table in tables)
    {
        var props = table.GetProperties()
                        .Where(p => p.ClrType == typeof(byte[])
                        && p.ValueGenerated == Microsoft.EntityFrameworkCore.Metadata.ValueGenerated.OnAddOrUpdate
                        && p.IsConcurrencyToken);

        var tableName = table.Relational().TableName;

        foreach (var field in props)
        {
            string[] SQLs = new string[] {
                $@"CREATE TRIGGER Set{tableName}_{field.Name}OnUpdate
                AFTER UPDATE ON {tableName}
                BEGIN
                    UPDATE {tableName}
                    SET RowVersion = randomblob(8)
                    WHERE rowid = NEW.rowid;
                END
                ",
                $@"CREATE TRIGGER Set{tableName}_{field.Name}OnInsert
                AFTER INSERT ON {tableName}
                BEGIN
                    UPDATE {tableName}
                    SET RowVersion = randomblob(8)
                    WHERE rowid = NEW.rowid;
                END
                "
            };

            foreach (var sql in SQLs)
            {
                using (var command = connection.CreateCommand())
                {
                    command.CommandText = sql;
                    command.ExecuteNonQuery();
                }
            }
        }
    }
}
0 голосов
/ 09 октября 2018

Это потому, что вы используете Guid:

public Guid Id { get; set; }

Эта проблема обсуждается и воспроизводится в Gitub :

Ошибка здесь из-зав свойство ApplicationUser.ConcurrencyStamp.ApplicationUser в удостоверении использует ConcurrencyStamp типа Guid для параллелизма.При создании нового класса он устанавливает значение NewGuid ().Когда вы создаете новое ApplicationUser, подобное этому, и устанавливаете его состояние в Modified EF Core, в базе данных нет данных о том, что было ConcurrencyStamp.Следовательно, он будет использовать любое значение, установленное для элемента (которое будет NewGuid ()), поскольку это значение отличается от значения в базе данных, и оно используется в предложении where оператора update, исключение выдает, что 0 строк изменено, когда ожидается 1.

При обновлении сущности токеном параллелизма вы не можете создать новый объект и отправить обновление напрямую.Вы должны извлечь запись из базы данных (чтобы у вас было значение ConcurrencyStamp), затем обновить запись и вызвать SaveChanges.Поскольку ApplicationUser.ConcurrencyStamp является токеном параллелизма на стороне клиента, вам также необходимо сгенерировать NewGuid () при обновлении записи.Таким образом, он может обновить значение в базе данных.

Подробнее о том, как работать с ApplicationUser.ConcurrencyStamp , можно узнать здесь .

Добро пожаловать на сайт PullRequest, где вы можете задавать вопросы и получать ответы от других членов сообщества.
...