在EF Core中删除具有并发性的实体

时间:2017-12-20 20:49:53

标签: c# entity-framework entity-framework-core

我想删除EF Core中的实体而不先从数据库中加载它。我知道以前曾问过类似的问题,但请耐心等待,因为这种情况不同。除了通常的ID之外,该实体还有一个行版本,这会导致问题。

实体的定义如下:

public int MyEntity {
    public int Id { get; set; }
    //Other irrelevant properties
    public byte[] RowVersion { get; set; }
}

使用流畅的API配置实体:

class MyEntityConfiguration : IEntityTypeConfiguration<MyEntity> {
    public void Configure( EntityTypeBuilder<MyEntity> builder ) {
        builder.Property( e => e.RowVersion )
            .IsRequired()
            .IsRowVersion();
    }
}

行版本允许我进行乐观并发检查。问题是,当我尝试删除实体而不首先加载它时......

void RemoveMyEntity( int id ) {
    MyEntity removeEntity = new MyEntity {
        Id = id
    };
    m_context.MyEntities.Attach( removeEntity );
    m_context.MyEntities.Remove( removeEntity );
}

...并发检查落在我的脚上。我在DbUpdateConcurrencyException

中收到此错误消息
  

数据库操作预计会影响1行但实际上会影响0行。自实体加载以来,数据可能已被修改或删除。

原因是EF Core生成此查询以删除项目:

exec sp_executesql N'SET NOCOUNT ON;
DELETE FROM [MyEntity]
WHERE [Id] = @p57 AND [RowVersion] IS NULL;
SELECT @@ROWCOUNT',N'@p57 int',@p57=1 -- <--The ID of the item to delete

问题显然在AND [RowVersion] IS NULL。这个条件永远不会<{1}},因为(就像我在配置实体时已明确告诉EF),该列是必需的,因此不能是true

当然,我没有在我要删除的实体中添加行版本,实际上我不想添加行版本,因为这意味着我必须从数据库中获取数据,这不是在这种情况下是必要的我甚至不介意在这里进行并发检查,因为如果之前删除了项目,它就不会受到影响。

所以问题是:有没有办法忽略对此操作的并发检查(但不是同一事务中的其他操作)或者以另一种方式使删除工作,而不必先从数据库中获取行版本

2 个答案:

答案 0 :(得分:0)

不确定EF Core,但我对EF 6使用以下变通办法:我创建了从主要继承的其他NoConcurencyDbContext,覆盖OnModelCreating并将所有RowVersion属性配置为ConcurencyMode.None

publicclass NoConcurencyDbContext : MainDbContext {

    protected override void OnModelCreating(DbModelBuilder modelBuilder)
    {
        base.OnModelCreating(modelBuilder);
        modelBuilder.Entity<EntityWithRowVersio>().Property(t => t.RowVersion).IsConcurrencyToken(false);

    }
}

您可以编写T4模板并从EF模型自动生成此类上下文。 因此,此想法很简单,即在子dbcontext中针对特定操作修改模型配置。 希望它也可以在EF Core中完成

答案 1 :(得分:-1)

您可以按照以下粘贴和解释的模式进行操作。

1)您必须在模型和数据库上定义一个行版本字段。

2)对于删除,只需提供ID并在显示其进行编辑更新或删除时提供您已经检索到的实体的行版本,则至少必须具有该ID。

3)如果您没有行版本,则不在乎,它将默认为null。

模式如下:

a)检查是否在变更跟踪器上跟踪了该实体,如果是,则将其删除。

b)如果未跟踪该实体,则它将使用ID和您拥有的行版本(或null)来模拟一个新的假实体。

c)哈哈!您没有提供行版本,则必须强行,您必须从数据库中提供一个有效的版本,然后从数据库中选择一个(使用dbset Find()方法或..)。

(就我而言,我使用的是标量t-sql函数,该函数是从db上下文执行的:https://docs.microsoft.com/en-us/ef/core/what-is-new/ef-core-2.0)。

d)最后断言删除成功返回true (稍后..调用保存更改时,dbcontext将保留更改或爆炸!!!,由您来处理错误)

我将从通用存储库粘贴一些代码:

public async Task<bool> DeleteByIdAsync(TKey id, byte[] rowVersion = null, CancellationToken? token = null) 
{
    var entityTracked = _context.ChangeTracker
        .Entries<TEntity>()
        .Select((EntityEntry e) => (TEntity)e.Entity)
        .SingleOrDefault(e => e.Id.Equals(id));                                                             

    if (entityTracked != null)                                                                         
    {
        _context.Remove(entityTracked);                                                                     // If entity is tracked just delete it
    }
    else                                                                                                    // If not tracked, just mock up a new entity.
    {
        var entityMocked = new TEntity { Id = id, RowVersion = rowVersion };                                // (*) ValidateModelContext extension custom code will not validate for EntityState.Deleted to avoid moked entity fail validations. We are going to delete it anyway so has no sense to validate it before deleting it.

        if (rowVersion == null)                                                                             // DUDE!! Why you do not pass me a valid row version?. Then I forcelly must do a database trip to get the rowversion for this id. We do it executing an scalar function that returns me current row version value for this entity. 
            entityMocked.RowVersion = await GetRowVersionAsync(id.ToString()).ConfigureAwait(false);        // If the record do not exist on the database a null is returned. In such case, even if we already know that something went wrong, we let the dbconcurrency error occurs when saving changes since is really a concurency error, since the record not longer exists.

        _context.Remove(entityMocked);                                                                      // Just delete it it.
    }

    return true;
}

t-sql就像:

CREATE FUNCTION [dbo].[GetDepartmentRowVersion] (@Id INT) RETURNS BINARY(8) AS BEGIN
DECLARE @rowVersion AS BINARY(8)
IF @Id = 0 SELECT @rowVersion = MAX([RowVersion]) FROM Department ELSE SELECT @rowVersion = [RowVersion] FROM Department WHERE Id = @Id
RETURN @rowVersion
END
GO

db上下文上用于映射这些udf的代码如下:

public async Task<byte[]> GetRowVersionAsync<TEntity>(string id = null)
{
    switch (typeof(TEntity))
    {
        case var type when type == typeof(Department):
            return await Department.Select(e => GetDepartmentRowVersion(Convert.ToInt32(id))).FirstOrDefaultAsync();

        case var type when type == typeof(Player):
            return await (id == null ? Player.Select(e => GetPlayerRowVersion(Guid.Empty)) : Player.Select(e => GetPlayerRowVersion(new Guid(id)))).FirstOrDefaultAsync();

        case var type when type == typeof(Address):
            return await (id == null ? Address.Select(e => GetAddressRowVersion(Guid.Empty)) : Address.Select(e => GetAddressRowVersion(new Guid(id)))).FirstOrDefaultAsync();

        default:
            return new byte[] { };
    }
}

public static byte[] GetPlayerRowVersion(Guid id) => null;     // Scalar function mappings.
public static byte[] GetAddressRowVersion(Guid id) => null;    // When using an in memory database since SQL UDF functions are not mapped to any Server, so we return a null value instead of throwing an error.
public static byte[] GetDepartmentRowVersion(int id) => null;

您可以构建特定的迁移来更新ufs,如下所示:

    [DbContext(typeof(DataContext))]
    [Migration(nameof(DataContext) + nameof(GetDepartmentRowVersion))]
    public class GetDepartmentRowVersion : Migration
    {
        protected override void Up(MigrationBuilder builder)
        {
            var sp = $@"
CREATE FUNCTION {nameof(DataContext.GetDepartmentRowVersion)} (@Id INT) RETURNS BINARY(8) AS BEGIN
DECLARE @rowVersion AS BINARY(8)
IF @Id = 0 SELECT @rowVersion = MAX([{nameof(Department.RowVersion)}]) FROM {nameof(DataContext.Department)} ELSE SELECT @rowVersion = [{nameof(Department.RowVersion)}] FROM {nameof(DataContext.Department)} WHERE {nameof(Department.Id)} = @Id
RETURN @rowVersion
END";

            if (builder.ActiveProvider.Split('.').Last() == StorageProvider.SqlServer.ToString()) builder.Sql(sp);
        }

        protected override void Down(MigrationBuilder builder)
        {
            var sp = $@"IF OBJECT_ID('{nameof(DataContext.GetDepartmentRowVersion)}') IS NOT NULL DROP FUNCTION {nameof(DataContext.GetDepartmentRowVersion)}";

            if (builder.ActiveProvider.Split('.').Last() == StorageProvider.SqlServer.ToString()) builder.Sql(sp);
        }
    }

我希望您能保留关键概念。BR和愉快的生活