繁体   English   中英

如何使用“Entity Framework Core”(又名 EF7)实现“软删除”?

[英]How can I implement “Soft Deletes” with “Entity Framework Core” (aka EF7)?

我正在尝试使用 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 (或 true)的所有记录。 我不想在每个查询结束时都要求&& x.IsDeleted == false

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

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

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

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

Wiki: EF 查询过滤器

编辑:回答子问题

仔细解释一下这在幕后是如何工作的?

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

// Filter by global configuration
QueryFilterManager.Filter<Customer>(q => q.Where(x => x.IsActive));
var ctx = new EntitiesContext();
// TIP: You can also add this line in EntitiesContext constructor instead
QueryFilterManager.InitilizeGlobalFilter(ctx);

// Filter by instance configuration
var ctx = new EntitiesContext();
ctx.Filter<Post>(MyEnum.EnumValue, q => q.Where(x => !x.IsSoftDeleted)).Disable();

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

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

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

如果您想了解它的工作原理,该代码是免费开源的

编辑:回答子问题

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

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

如果您可以迁移到 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);
    }
}

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

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


...

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设置,如图所示下面。

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

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

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

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

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

此行为在文档页面底部进行了讨论。

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

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

暂无
暂无

声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM