EF 是否可以自动删除未删除父级的孤立数据

本文关键字:删除 数据 EF 是否 | 更新日期: 2023-09-27 18:35:49

对于使用Code First EF 5 beta的应用程序,我有:

public class ParentObject
{
    public int Id {get; set;}
    public virtual List<ChildObject> ChildObjects {get; set;}
    //Other members
}

public class ChildObject
{
    public int Id {get; set;}
    public int ParentObjectId {get; set;}
    //Other members
}

必要时,相关的 CRUD 操作由存储库执行。

OnModelCreating(DbModelBuilder modelBuilder)

我已经设置了它们:

modelBuilder.Entity<ParentObject>().HasMany(p => p.ChildObjects)
            .WithOptional()
            .HasForeignKey(c => c.ParentObjectId)
            .WillCascadeOnDelete();

因此,如果删除了ParentObject,则其子对象也会被删除。

但是,如果我运行:

parentObject.ChildObjects.Clear();
_parentObjectRepository.SaveChanges(); //this repository uses the context

我得到例外:

操作失败:无法更改关系,因为一个或多个外键属性不可为空。对关系进行更改时,相关的外键属性将设置为 null 值。如果外键不支持空值,则必须定义新关系,必须为外键属性分配另一个非空值,或者必须删除不相关的对象。

这是有道理的,因为实体的定义包括正在被破坏的外键约束。

我是否可以将实体配置为在孤立时"清除自身",或者必须手动从上下文中删除这些ChildObject(在本例中使用 ChildObjectRepository)。

EF 是否可以自动删除未删除父级的孤立数据

它实际上是受支持的,但仅当您使用标识关系时。它也适用于代码。您只需要为包含IdParentObjectIdChildObject定义复杂键:

modelBuilder.Entity<ChildObject>()
            .HasKey(c => new {c.Id, c.ParentObjectId});

由于定义此类键将删除自动递增 ID 的默认约定,因此您必须手动重新定义它:

modelBuilder.Entity<ChildObject>()
            .Property(c => c.Id)
            .HasDatabaseGeneratedOption(DatabaseGeneratedOption.Identity);

现在调用 parentObject.ChildObjects.Clear() 会删除依赖对象。

顺便说一句,您的关系映射应该使用 WithRequired 来遵循您的真实类,因为如果 FK 不可为空,则它不是可选的:

modelBuilder.Entity<ParentObject>().HasMany(p => p.ChildObjects)
            .WithRequired()
            .HasForeignKey(c => c.ParentObjectId)
            .WillCascadeOnDelete();

更新:

我找到了一种方法,不需要将导航属性从子实体添加到父实体或设置复杂键。

它基于本文,该文章使用ObjectStateManager查找已删除的实体。

有了ObjectStateEntry的列表,我们可以从每个列表中找到一对EntityKey,它代表已删除的关系。

在这一点上,我找不到任何必须删除的指示。与文章的示例相反,如果子项具有返回父项的导航属性,则只需选择第二个项即可删除父项。因此,为了解决这个问题,我跟踪应该使用类OrphansToHandle处理哪些类型。

模型:

public class ParentObject
{
    public int Id { get; set; }
    public virtual ICollection<ChildObject> ChildObjects { get; set; }
    public ParentObject()
    {
        ChildObjects = new List<ChildObject>();
    }
}
public class ChildObject
{
    public int Id { get; set; }
}

其他类:

public class MyContext : DbContext
{
    private readonly OrphansToHandle OrphansToHandle;
    public DbSet<ParentObject> ParentObject { get; set; }
    public MyContext()
    {
        OrphansToHandle = new OrphansToHandle();
        OrphansToHandle.Add<ChildObject, ParentObject>();
    }
    public override int SaveChanges()
    {
        HandleOrphans();
        return base.SaveChanges();
    }
    private void HandleOrphans()
    {
        var objectContext = ((IObjectContextAdapter)this).ObjectContext;
        objectContext.DetectChanges();
        var deletedThings = objectContext.ObjectStateManager.GetObjectStateEntries(EntityState.Deleted).ToList();
        foreach (var deletedThing in deletedThings)
        {
            if (deletedThing.IsRelationship)
            {
                var entityToDelete = IdentifyEntityToDelete(objectContext, deletedThing);
                if (entityToDelete != null)
                {
                    objectContext.DeleteObject(entityToDelete);
                }
            }
        }
    }
    private object IdentifyEntityToDelete(ObjectContext objectContext, ObjectStateEntry deletedThing)
    {
        // The order is not guaranteed, we have to find which one has to be deleted
        var entityKeyOne = objectContext.GetObjectByKey((EntityKey)deletedThing.OriginalValues[0]);
        var entityKeyTwo = objectContext.GetObjectByKey((EntityKey)deletedThing.OriginalValues[1]);
        foreach (var item in OrphansToHandle.List)
        {
            if (IsInstanceOf(entityKeyOne, item.ChildToDelete) && IsInstanceOf(entityKeyTwo, item.Parent))
            {
                return entityKeyOne;
            }
            if (IsInstanceOf(entityKeyOne, item.Parent) && IsInstanceOf(entityKeyTwo, item.ChildToDelete))
            {
                return entityKeyTwo;
            }
        }
        return null;
    }
    private bool IsInstanceOf(object obj, Type type)
    {
        // Sometimes it's a plain class, sometimes it's a DynamicProxy, we check for both.
        return
            type == obj.GetType() ||
            (
                obj.GetType().Namespace == "System.Data.Entity.DynamicProxies" &&
                type == obj.GetType().BaseType
            );
    }
}
public class OrphansToHandle
{
    public IList<EntityPairDto> List { get; private set; }
    public OrphansToHandle()
    {
        List = new List<EntityPairDto>();
    }
    public void Add<TChildObjectToDelete, TParentObject>()
    {
        List.Add(new EntityPairDto() { ChildToDelete = typeof(TChildObjectToDelete), Parent = typeof(TParentObject) });
    }
}
public class EntityPairDto
{
    public Type ChildToDelete { get; set; }
    public Type Parent { get; set; }
}

原始答案

要在不设置复杂键的情况下解决此问题,您可以覆盖DbContextSaveChanges,但随后使用 ChangeTracker 来避免访问数据库以查找孤立对象。

首先向ChildObject添加一个导航属性(如果需要,可以保留int ParentObjectId属性,无论哪种方式都可以):

public class ParentObject
{
    public int Id { get; set; }
    public virtual List<ChildObject> ChildObjects { get; set; }
}
public class ChildObject
{
    public int Id { get; set; }
    public virtual ParentObject ParentObject { get; set; }
}

然后使用 ChangeTracker 查找孤立对象:

public class MyContext : DbContext
{
    //...
    public override int SaveChanges()
    {
        HandleOrphans();
        return base.SaveChanges();
    }
    private void HandleOrphans()
    {
        var orphanedEntities =
            ChangeTracker.Entries()
            .Where(x => x.Entity.GetType().BaseType == typeof(ChildObject))
            .Select(x => ((ChildObject)x.Entity))
            .Where(x => x.ParentObject == null)
            .ToList();
        Set<ChildObject>().RemoveRange(orphanedEntities);
    }
}

您的配置将变为:

modelBuilder.Entity<ParentObject>().HasMany(p => p.ChildObjects)
            .WithRequired(c => c.ParentObject)
            .WillCascadeOnDelete();

我做了一个简单的速度测试,迭代了 10.000 次。启用HandleOrphans()后,完成时间为 1:01.443 分钟,禁用后为 0:59.326 分钟(两者平均运行三次)。测试下面的代码。

using (var context = new MyContext())
{
    var parentObject = context.ParentObject.Find(1);
    parentObject.ChildObjects.Add(new ChildObject());
    context.SaveChanges();
}
using (var context = new MyContext())
{
    var parentObject = context.ParentObject.Find(1);
    parentObject.ChildObjects.Clear();
    context.SaveChanges();
}

想分享另一个对我有用的 .net ef core 解决方案,可能有人会发现它很有用。

有一个带有两个外键(要么或)的子表,所以接受的解决方案对我不起作用。根据马科斯·迪米特里奥(Marcos Dimitrio)的回答,我想出了以下几点:

在我的自定义 DbContext 中:

public override async Task<int> SaveChangesAsync(CancellationToken cancellationToken = new CancellationToken())
  {
    var modifiedEntities = this.ChangeTracker.Entries().Where(c => c.State == EntityState.Modified);
    foreach (var entityEntry in modifiedEntities)
    {
      if (entityEntry.Entity is ChildObject)
      {
         var fkProperty = entityEntry.Property(nameof(ChildObject.ParentObjectId));
         if (fkProperty.IsModified && fkProperty.CurrentValue == null && fkProperty.OriginalValue != null)
         {
           // Checked if FK was set to NULL
           entityEntry.State = EntityState.Deleted;
         }
      }
    }
    return await base.SaveChangesAsync(cancellationToken);
  }

在 EF Core 中,可以通过删除孤立项来完成此操作。

喜欢这个:

dbContext.Children.Clear();

这是我对实体框架 6.4.4 的通用解决方案,无需了解特定架构。

请注意,我

从修改的实体条目开始搜索孤立实体,因为在我的情况下,我找不到任何搜索已删除关系条目的内容,就像其他答案所建议的那样。

该方法背后的逻辑是,从所需关系的集合中删除的实体的外键将由实体框架更新为 null。因此,我们搜索所有修改的实体,这些实体至少有一个关系,其结尾具有多重性"一",但外键设置为 null。

将此方法添加到DbContext子类中。您可以重写 SaveChanges/SaveChangesAsync 方法以自动调用此方法。

public void DeleteOrphanEntries()
{
  this.ChangeTracker.DetectChanges();
  var objectContext = ((IObjectContextAdapter)this).ObjectContext;
  var orphanEntityEntries =
    from entry in objectContext.ObjectStateManager.GetObjectStateEntries(EntityState.Modified)
    where !entry.IsRelationship
    let relationshipManager = entry.RelationshipManager
    let orphanRelatedEnds = from relatedEnd in relationshipManager.GetAllRelatedEnds().OfType<EntityReference>()
                            where relatedEnd.EntityKey == null // No foreign key...
                            let associationSet = (AssociationSet)relatedEnd.RelationshipSet
                            let associationEndMembers = from associationSetEnd in associationSet.AssociationSetEnds
                                                        where associationSetEnd.EntitySet != entry.EntitySet // ... not the end pointing to the entry
                                                        select associationSetEnd.CorrespondingAssociationEndMember
                            where associationEndMembers.Any(e => e.RelationshipMultiplicity == RelationshipMultiplicity.One) // ..but foreign key required.
                            select relatedEnd
    where orphanRelatedEnds.Any()
    select entry;
  foreach (var orphanEntityEntry in orphanEntityEntries)
  {
    orphanEntityEntry.Delete();
  }
}

是的。以下内容在 EF Core 中工作:

确保将级联行为设置为如下所示Cascade

entity.HasOne(d => d.Parent)
                    .WithMany(p => p.Children)
                    .HasForeignKey(d => d.ParentId)
                    .OnDelete(DeleteBehavior.Cascade);

然后在要删除的所有子实体中将 Parent 属性设置为 null,如下所示:

var childrenToBeRemoved = parent.Children.Where(filter);
foreach(var child in childrenToBeRemoved)
{
    child.Parent = null;
}

现在,context.SaveAsync()应删除所有孤立子实体。

EF 目前不支持此功能。您可以通过覆盖上下文中的 SaveChanges 并手动删除不再具有父对象的子对象来实现此目的。代码将是这样的:

public override int SaveChanges()
{
    foreach (var bar in Bars.Local.ToList())
    {
        if (bar.Foo == null)
        {
            Bars.Remove(bar);
        }
    }
    return base.SaveChanges();
}