我该如何实现" Soft Deletes"与"实体框架核心" (又名EF7)?

时间:2016-06-20 21:48:40

标签: c# entity-framework-core

我试图实施"软删除"使用EF7。我的Item表中有一个名为IsDeleted的字段bit。我在SO和其他地方看到的所有例子都使用了这样的东西:

protected override void OnModelCreating(ModelBuilder modelBuilder)
{
    modelBuilder.Entity<Item>().Map(m => m.Requires("IsDeleted").HasValue(false));
}

Map()不再是ModelBuilder的方法。

编辑:让我澄清一下。我现在大多只对阅读数据感兴趣。我希望EF自动过滤掉Item表中IsDeleted == 1(或者真)的所有记录。我不想在每个查询结束时要求&& x.IsDeleted == false

3 个答案:

答案 0 :(得分:2)

免责声明:我是该项目的所有者Entity Framework Plus

正如您将在@Adem链接中看到的,我们的库支持查询过滤。

您可以轻松启用/禁用全局/实例过滤器

true/false

维基:EF Query Filter

修改:回答子问题

  

注意解释这是如何在幕后工作的?

首先,您可以全局或按实例初始化过滤器

QueryFilterManager.Filter<Item>(q => q.Where(x => !x.IsDeleted));

在幕后,库将循环上下文的每个DbSet,并检查过滤器是否可以应用于泛型类型。

在这种情况下,库将使用过滤器从DbSet过滤原始/过滤的查询,然后修改新过滤查询的当前内部查询。

总之,我们更改了一些DbSet内部值以使用过滤后的查询。

代码免费&amp; 开源如果您想了解它的工作原理。

修改:回答子问题

  

@jonathan这个过滤器也会包含导航集合吗?

对于EF Core,由于Interceptor尚不可用,因此尚不支持。但是从EF Core 2.x开始,EF团队已经实现了全局查询过滤器,应该允许这样做。

答案 1 :(得分:1)

如果您可以迁移到EF Core 2.0,则可以使用 模型级查询过滤器 https://docs.microsoft.com/en-us/ef/core/what-is-new/index

如果您使用EF Core 1.0 您可以使用可用的EF Core功能制作一些技巧:

继承 https://docs.microsoft.com/en-us/aspnet/core/data/ef-mvc/inheritance

暗影属性 https://docs.microsoft.com/en-us/ef/core/modeling/shadow-properties

public class Attachment : AttachmentBase
{}

public abstract class AttachmentBase
{
    public const string StatePropertyName = "state";

    public Guid Id { get; set; }
}

public enum AttachmentState
{
    Available,
    Deleted
}

public class AttachmentsDbContext : DbContext
{
    public AttachmentsDbContext(DbContextOptions options)
        : base(options)
    {
    }

    public DbSet<Attachment> Attachments { get; set; }

    public override Task<int> SaveChangesAsync(CancellationToken cancellationToken = default(CancellationToken))
    {
        IEnumerable<EntityEntry<Attachment>> softDeletedAttachments = ChangeTracker.Entries<Attachment>().Where(entry => entry.State == EntityState.Deleted);

        foreach (EntityEntry<Attachment> softDeletedAttachment in softDeletedAttachments)
        {
            softDeletedAttachment.State = EntityState.Modified;
            softDeletedAttachment.Property<int>(AttachmentBase.StatePropertyName).CurrentValue = (int)AttachmentState.Deleted;
        }
        return base.SaveChangesAsync(cancellationToken);
    }

    protected override void OnModelCreating(ModelBuilder modelBuilder)
    {
        modelBuilder.Entity<AttachmentBase>()
            .HasDiscriminator<int>(AttachmentBase.StatePropertyName)
            .HasValue<Attachment>((int)AttachmentState.Available);

        modelBuilder.Entity<AttachmentBase>().Property<int>(AttachmentBase.StatePropertyName).Metadata.IsReadOnlyAfterSave = false;

        modelBuilder.Entity<Attachment>()
            .ToTable("available_attachment");

        modelBuilder.Entity<AttachmentBase>()
            .ToTable("attachment");

        base.OnModelCreating(modelBuilder);
    }
}

答案 2 :(得分:1)

现在是 2021 年,我想到要添加一个更现代、更标准的内置解决方案,该解决方案适用于当前版本的 EF Core。

使用 global query filters,您可以确保某些过滤器始终应用于某些实体。您可以通过界面定义软删除属性,这有助于以编程方式将过滤器添加到所有相关实体。见:


...

public interface ISoftDeletable
{
    public string DeletedBy { get; }
    public DateTime? DeletedAt { get; }
}

...

// Call it from DbContext.OnModelCreating()
private static void ConfigureSoftDeleteFilter(ModelBuilder builder)
{
    foreach (var softDeletableTypeBuilder in builder.Model.GetEntityTypes()
        .Where(x => typeof(ISoftDeletable).IsAssignableFrom(x.ClrType)))
    {
        var parameter = Expression.Parameter(softDeletableTypeBuilder.ClrType, "p");

        softDeletableTypeBuilder.SetQueryFilter(
            Expression.Lambda(
                Expression.Equal(
                    Expression.Property(parameter, nameof(ISoftDeletable.DeletedAt)),
                    Expression.Constant(null)),
                parameter)
        );
    }
}

然后,确保在删除期间使用此标志而不是硬删除(替代例如存储库设置标志而不是删除实体):

public override Task<int> SaveChangesAsync(bool acceptAllChangesOnSuccess, CancellationToken cancellationToken = default)
{
    foreach (var entry in ChangeTracker.Entries<ISoftDeletable>())
    {
        switch (entry.State)
        {
            case EntityState.Deleted:
                // Override removal. Unchanged is better than Modified, because the latter flags ALL properties for update.
                // With Unchanged, the change tracker will pick up on the freshly changed properties and save them.
                entry.State = EntityState.Unchanged;
                entry.Property(nameof(ISoftDeletable.DeletedBy)).CurrentValue = _currentUser.UserId;
                entry.Property(nameof(ISoftDeletable.DeletedAt)).CurrentValue = _dateTime.Now;
                break;
        }
    }
    return base.SaveChangesAsync(acceptAllChangesOnSuccess, cancellationToken);
}

注意事项 1:级联删除时序

一个关键方面是考虑到相关实体的级联删除,要么禁用级联删除,要么了解和控制 EF Core 的级联删除时序行为。 CascadeDeleteTiming 设置的默认值为 CascadeTiming.Immediate,这会导致 EF Core 立即将“已删除”实体的所有导航属性标记为 EntityState.Deleted,并恢复 EntityState.Deleted 状态只有在根实体上不会在导航属性上恢复它。因此,如果您有不使用软删除的导航属性,并且您想避免它们被删除,您也必须处理它们的更改跟踪器状态(而不是仅处理 ISoftDeletable 实体),或更改CascadeDeleteTiming 设置如下所示。

软删除实体上使用的 owned types 也是如此。使用默认的删除级联计时,EF Core 还将这些拥有的类型标记为“已删除”,如果它们被设置为必需/不可为空,则在尝试保存软删除的实体时会遇到 SQL 更新失败。

public ApplicationDbContext(DbContextOptions<ApplicationDbContext> options) : base(options)
{
    ChangeTracker.CascadeDeleteTiming = CascadeTiming.OnSaveChanges;
}

警告 2:对其他根实体的影响

如果您以这种方式定义全局查询过滤器,EF Core 将努力隐藏引用软删除实体的所有其他实体。

例如,如果您软删除了一个 Partner 实体,并且您有 Order 个实体,其中每个实体都通过(必需的)导航属性引用了一个合作伙伴,那么当您检索订单列表并且您包括合作伙伴,所有引用软删除的 Partner 的订单都将从列表中丢失。

此行为在 bottom of the documentation page 中讨论。

遗憾的是,EF Core 5 中的全局查询过滤器不提供将它们限制为根实体或仅禁用其中一个过滤器的选项。唯一可用的选项是使用 IgnoreQueryFilters() 方法,该方法禁用所有过滤器。由于 IgnoreQueryFilters() 方法采用 IQueryable 并返回 IQueryable,因此您不能使用此方法为公开的 DbSet 透明地禁用 DbContext 类中的过滤器。< /p>

不过,一个重要的细节是,只有在查询时Include() 给定的导航属性才会发生这种情况。并且有一个有趣的解决方案来获取一个结果集,该结果集具有应用于某些实体的查询过滤器,但没有将它们应用于其他实体,依赖于 EF 鲜为人知的功能,关系修复。基本上,您加载具有导航属性 EntityA(不包括 EntityB)的 EntityB 列表。然后使用 EntityB 单独加载 IgnoreQueryFilters() 的列表。发生的情况是 EF 自动将 EntityB 上的 EntityA 导航属性设置为加载的 EntityB 实例。通过这种方式,查询过滤器应用于 EntityA 本身,但并未应用于 EntityB 导航属性,因此即使使用软删除的 EntityA,您也可以看到 EntityB s。 See this answer on another question。 (当然这对性能有影响,你仍然不能把它封装在 DbContext 中。)