Правильное моделирование RowVersion в SqLite с преобразованием в ulong на Entity Framework Core

Я пытаюсь заставить RowVersion работать правильно как на SqLite, так и на SqlServer с помощью простого запроса в столбце rowversion. Чтобы иметь возможность сделать это, мне нужно преобразовать столбец rowversion в ulong вместо byte[], и он по-прежнему будет работать правильно.

public abstract class VersionEntity
{
    public ulong RowVersion { get; set; }
}

public class Observation : VersionEntity
{
    public Guid Id { get; set; }
    public Guid TaskId { get; set; }
    public string Description { get; set; }
    public DateTime DueDate { get; set; }
    public Severity Severity { get; set; }
}

public class TestDbContext : DbContext
{
    public static string ConnectionString { get; set; } = "Data Source=dummy.db";
    public DbSet<Observation> Observation { get; set; }

    protected override void OnModelCreating(ModelBuilder modelBuilder)
    {
         modelBuilder.Entity<Observation>().HasKey(o => o.Id);
         modelBuilder.Entity<Observation>().Property(o => o.RowVersion).HasConversion(new NumberToBytesConverter<ulong>()).IsRowVersion();
    }

    protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder)
    {
        optionsBuilder.UseSqlite(ConnectionString);
        optionsBuilder.UseLazyLoadingProxies();
    }

}

Внутри моей первой миграции добавления я изменил RowVersion на rowVersion: true (автоматически не добавлялся). Также добавлено

private string _triggerQuery = @"CREATE TRIGGER Set{0}RowVersion{1}
   AFTER {1} ON {0}
   BEGIN
      UPDATE {0}
      SET RowVersion = current_timestamp
      WHERE rowid = NEW.rowid;
   END
";

migrationBuilder.Sql(String.Format(_triggerQuery, tableName, "UPDATE"));
migrationBuilder.Sql(String.Format(_triggerQuery, tableName, "INSERT"));

Таким образом, он создается с помощью триггеров для имитации добавочного глобального значения SqlServer RowVersion.

Миграция работает, и первое сохранение работает

context.Database.Migrate();
var id = Guid.NewGuid();
context.Observation.Add(new Observation
{
    Id = id,
    Description = "Test description1",
    TaskId = Guid.NewGuid(),
    Severity = Severity.Low,
    DueDate = DateTime.Now
});
context.Observation.Add(new Observation
{
    Id = Guid.NewGuid(),
    Description = "Test description2",
    TaskId = Guid.NewGuid(),
    Severity = Severity.Low,
    DueDate = DateTime.Now
});
context.SaveChanges(); // This works, and saves data
var observation = context.Observation.FirstOrDefault(o => o.Id == id);
observation.Description = "changed.."; // Checking here will show a value on RowVersion property
context.SaveChanges(); // This fail with concurrency error

Ошибка параллелизма: ожидается, что операция базы данных повлияет на 1 строку (строки), но на самом деле затронет 0 строк. Данные могли быть изменены или удалены после загрузки сущностей. См. http://go.microsoft.com/fwlink/?LinkId=527962. для получения информации о понимании и обработке исключений оптимистического параллелизма.

Я не понимаю, почему это должно быть проблемой. Кто-нибудь знает, почему это не работает? Выбранный Entity, похоже, имеет значение свойства RowVersion. Но когда он сохранен, он думает, что изменился.


person Atle S    schedule 18.10.2019    source источник
comment
current_timestamp возвращает строковую дату в sqlite.   -  person C Perkins    schedule 18.10.2019


Ответы (2)


Я отказался от попыток использовать IsRowVersion с SqLite.

Закончилось установкой типа RowVersion на long и использованием IsConcurrencyToken().ValueGeneratedOnAddOrUpdate().HasDefaultValue(0); вместо IsRowVersion().HasConversion(..)

Также используется juliianday вместо current_timestamp

private static string _triggerQuery = @"CREATE TRIGGER Set{0}RowVersion{1}
        AFTER {1} ON {0}
        BEGIN
            UPDATE {0}
            SET RowVersion = CAST(ROUND((julianday('now') - 2440587.5)*86400000) AS INT)
            WHERE rowid = NEW.rowid;
        END
    ";

migrationBuilder.Sql(String.Format(_triggerQuery, tableName, "UPDATE"));
migrationBuilder.Sql(String.Format(_triggerQuery, tableName, "INSERT"));

Теперь он работает правильно, и он переходит к мс для изменений. Таким образом, также легко запрашивать все изменения после определенного момента времени.

person Atle S    schedule 28.10.2019

Вероятно, это связано с тем, что значение RowVersion изменяется между двумя операциями SaveChanges.

Я предполагаю, что EntityFramework понимает RowVersion для SQL Server и будет использовать только RowVersion в качестве «маркера параллелизма». В базе данных SQLite он, вероятно, использует все поля в качестве токенов параллелизма, т.е. обновляет все поля в том виде, в котором мы их загрузили. Из-за триггера смоделированная RowVersion фактически изменилась, поэтому она считает, что существует проблема параллелизма.

Я не знаком с EF, но, возможно, вы можете указать ему исключить смоделированную RowVersion в качестве токена параллелизма или просто перезагрузить запись между обновлениями.

Я нашел это: https://www.infoworld.com/article/3085390/how-to-handle-concurrency-conflicts-in-entity-framework.html

person Etherman    schedule 18.10.2019
comment
Он помечен IsRowVersion. И это токен, который будет использоваться для проверки параллелизма. Это его основная цель. Но так как сущность извлекается, версия строки также извлекается. И любая проверка из Entity Framework должна увидеть, что это тот же объект с правильной RowVersion и обновить запись (предоставив ей новую версию строки после обновления). - person Atle S; 18.10.2019
comment
Выполняет ли EF повторную загрузку столбца RowVersion после сохранения объекта? Если это не так, значение в объекте будет устаревшим, поэтому вам может потребоваться повторно загрузить объект перед его повторным сохранением. - person Etherman; 18.10.2019
comment
Образец загружает его напрямую, так что это не проблема. Но да, когда он помечен с помощью IsRowVersion, он будет перезагружен после сохранения, чтобы сохранить правильное значение с сущностью. - person Atle S; 22.10.2019