简体   繁体   English

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

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

I'm trying to implement a "Soft Delete" using EF7.我正在尝试使用 EF7 实现“软删除”。 My Item table has a field named IsDeleted of type bit .我的Item表有一个名为IsDeleted的字段,类型为bit All of the examples that I see around SO and elsewhere are using something like this:我在 SO 和其他地方看到的所有例子都在使用这样的东西:

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

but Map() is no longer a method of ModelBuilder .Map()不再是ModelBuilder的方法。

EDIT: Let me clarify.编辑:让我澄清一下。 I'm mostly only interested in reading the data right now.我现在主要只对阅读数据感兴趣。 I want EF to automatically filter out all records in my Item table where IsDeleted == 1 (or true).我希望 EF 自动过滤掉我的Item表中IsDeleted == 1 (或 true)的所有记录。 I do not want to require an && x.IsDeleted == false at the end of every query.我不想在每个查询结束时都要求&& x.IsDeleted == false

Disclaimer : I'm the owner of the project Entity Framework Plus免责声明:我是Entity Framework Plus项目的所有者

As you will see in @Adem link, our library supports query filtering.正如您将在@Adem 链接中看到的,我们的库支持查询过滤。

You can easily enable/disable a global/instance filter您可以轻松启用/禁用全局/实例过滤器

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

Wiki: EF Query Filter Wiki: EF 查询过滤器

Edit : Answer sub question编辑:回答子问题

Care to explain how this works behind the scene?仔细解释一下这在幕后是如何工作的?

Firstly, you can either initialize filter globally or by instance首先,您可以全局或按实例初始化过滤器

// 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();

Under the hood, the library will loop on every DbSet of the context and checks if a filter can be applied to the generic type.在幕后,库将在上下文的每个 DbSet 上循环并检查过滤器是否可以应用于泛型类型。

In this case, the library will filter the original/filtered query from the DbSet using the filter then modify the current internal query for the new filtered query.在这种情况下,库将使用过滤器从 DbSet 过滤原始/过滤查询,然后修改当前内部查询以获取新的过滤查询。

In summary, we changed some DbSet internal value to use the filtered query.总之,我们更改了一些 DbSet 内部值以使用过滤查询。

The code is FREE & Open Source if you want to learn about how it works.如果您想了解它的工作原理,该代码是免费开源的

Edit : Answer sub question编辑:回答子问题

@jonathan will this filter included navigation collections too? @jonathan 这个过滤器也会包含导航集合吗?

For EF Core, it's not supported yet since Interceptor is not available yet.对于 EF Core,尚不支持,因为 Interceptor 尚不可用。 But starting from EF Core 2.x, the EF Team has implemented Global query filters which should allow this.但是从 EF Core 2.x 开始,EF 团队已经实现了应该允许这样做的全局查询过滤器。

If you can migrate to EF Core 2.0 you can use Model-level query filters https://docs.microsoft.com/en-us/ef/core/what-is-new/index如果您可以迁移到 EF Core 2.0,您可以使用模型级查询过滤器https://docs.microsoft.com/en-us/ef/core/what-is-new/index

If you use EF Core 1.0 You can make some trick with available EF Core features:如果您使用 EF Core 1.0,您可以使用可用的 EF Core 功能制作一些技巧:

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

Shadow properties https://docs.microsoft.com/en-us/ef/core/modeling/shadow-properties阴影属性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);
    }
}

It's 2021, and it occurred to me to add a more modern, standard, built-in solution that pertains to current versions of EF Core.现在是 2021 年,我想到添加一个更现代、更标准的内置解决方案,该解决方案与当前版本的 EF Core 相关。

With global query filters you can ensure that certain filters are always applied to certain entities.使用全局查询过滤器,您可以确保某些过滤器始终应用于某些实体。 And you can define your soft deletion properties via an interface, which facilitates programmatically adding the filter to all relevant entities.您可以通过界面定义软删除属性,这有助于以编程方式将过滤器添加到所有相关实体。 See:见:


...

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)
        );
    }
}

Then, to make sure this flag is used during deletion instead of hard deletion (alternative to eg repositories setting the flag instead of deleting the entity):然后,为了确保在删除期间使用此标志而不是硬删除(替代例如存储库设置标志而不是删除实体):

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);
}

Caveat 1: Cascade Delete Timing警告 1:级联删除时间

One crucial aspect is to take into account the cascade deletion of related entities, and either disable cascade delete, or understand and control the cascade delete timing behavior of EF Core.一个关键方面是考虑相关实体的级联删除,或者禁用级联删除,或者了解和控制 EF Core 的级联删除时序行为。 The default value of the CascadeDeleteTiming setting is CascadeTiming.Immediate , which causes EF Core to immediately flag all navigation properties of the 'deleted' entity as EntityState.Deleted , and reverting the EntityState.Deleted state only on the root entity won't revert it on the navigation properties. CascadeDeleteTiming设置的默认值是CascadeTiming.Immediate ,这会导致 EF Core 立即将“已删除”实体的所有导航属性标记为EntityState.Deleted ,并且仅在根实体上恢复EntityState.Deleted状态不会恢复它关于导航属性。 So if you have navigation properties which don't use soft deletion, and you want to avoid them being deleted, you must handle their change tracker state too (instead of just handling it for eg ISoftDeletable entities), or change the CascadeDeleteTiming setting as shown below.因此,如果您有不使用软删除的导航属性,并且您想避免它们被删除,您也必须处理它们的更改跟踪器状态(而不是仅处理ISoftDeletable实体等),或更改CascadeDeleteTiming设置,如图所示下面。

The same is true for owned types used on the soft-deleted entities.对于软删除实体上使用的拥有类型也是如此。 With the default deletion cascade timing EF Core also flags these owned types as 'deleted', and in case they are set as Required/non-nullable, you will encounter SQL update failures when trying to save the soft-deleted entities.使用默认的删除级联计时,EF Core 还将这些拥有的类型标记为“已删除”,如果它们被设置为必需/不可为空,则在尝试保存软删除的实体时会遇到 SQL 更新失败。

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

Caveat 2: Effect on other root entities警告 2:对其他根实体的影响

If you define a global query filter this way, EF Core will diligently hide all other entities that reference a soft-deleted entity.如果您以这种方式定义全局查询过滤器,EF Core 将努力隐藏引用软删除实体的所有其他实体。

For example if you've soft-deleted a Partner entity, and you have Order entities where each of them references a partner through a (required) navigation property, then, when you retrieve the list of orders and you include the partner, all orders that reference a soft-deleted Partner will be missing from the list.例如,如果您软删除了一个Partner实体,并且您有Order实体,其中每个实体都通过(必需的)导航属性引用一个合作伙伴,那么,当您检索订单列表并包含合作伙伴时,所有订单列表中将缺少引用软删除的Partner

This behavior is discussed at the bottom of the documentation page .此行为在文档页面底部进行了讨论。

Sadly, the global query filters as of EF Core 5 don't provide an option to limit them to root entities, or to disable only one of the filters.遗憾的是,EF Core 5 中的全局查询过滤器不提供将它们限制为根实体或仅禁用其中一个过滤器的选项。 The only available option is to use the IgnoreQueryFilters() method, which disables ALL filters.唯一可用的选项是使用IgnoreQueryFilters()方法,该方法禁用所有过滤器。 And since the IgnoreQueryFilters() method takes an IQueryable and also returns an IQueryable , you cannot use this method to transparently disable the filter inside your DbContext class for an exposed DbSet .由于IgnoreQueryFilters()方法采用IQueryable并返回IQueryable ,因此您不能使用此方法在 DbContext 类中为公开的DbSet透明地禁用过滤器。

Though, one important detail is that this occurs only if you Include() the given navigation property while querying.但是,一个重要的细节是,只有在查询时Include()给定的导航属性才会发生这种情况。 And there is an interesting solution for getting a result set that has query filters applied to certain entities but doesn't have them applied to other entities, relying on a lesser known feature of EF, relational fixup .并且有一个有趣的解决方案来获取一个结果集,该结果集具有应用于某些实体的查询过滤器,但没有将它们应用于其他实体,依赖于 EF 鲜为人知的特性, relational fixup Basically, you load a list of EntityA that has navigation property EntityB (without including EntityB ).基本上,您加载列表EntityA具有导航属性EntityB (不包括EntityB )。 And then you separately load the list of EntityB , using IgnoreQueryFilters() .然后使用IgnoreQueryFilters()单独加载EntityB的列表。 What happens is that EF automatically sets the EntityB navigation property on EntityA to the loaded EntityB instances.发生的情况是 EF 自动将EntityB上的EntityB导航属性EntityA为加载的EntityB实例。 This way the query filter was applied to EntityA itself, but wasn't applied to the EntityB navigational property, so you can see EntityA s even with soft-deleted EntityB s.通过这种方式,查询过滤器应用于EntityA本身,但未应用于EntityB导航属性,因此即使使用软删除的EntityB ,您也可以看到EntityA See this answer on another question .在另一个问题上看到这个答案 (Of course this has performance implications, and you still can't encapsulate it in DbContext.) (当然这会影响性能,您仍然无法将其封装在 DbContext 中。)

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

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