EF是否可以自动删除孤立的数据,而不删除父数据?

时间:2012-05-31 14:18:09

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

对于使用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,它的ChildObjects也是如此。

但是,如果我跑:

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

我得到例外:

  

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

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

我可以将实体配置为在孤立时“自我清理”,或者我必须从上下文中手动删除这些ChildObject(在这种情况下使用ChildObjectRepository)。

7 个答案:

答案 0 :(得分:31)

实际上支持它,但仅限于使用Identifying relation时。它首先与代码一起使用。您只需要为包含ChildObjectId的{​​{1}}定义复杂密钥:

ParentObjectId

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

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

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

顺便说一下。您的关系映射应使用modelBuilder.Entity<ChildObject>() .Property(c => c.Id) .HasDatabaseGeneratedOption(DatabaseGeneratedOption.Identity); 来跟踪您的真实类,因为如果FK不可为空,则它不是可选的:

WithRequired

答案 1 :(得分:4)

<强>更新

我找到了一种不需要从子节点向父实体添加导航属性或设置复杂密钥的方法。

它基于此article,使用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; }
}

原始答案

要在不设置复杂密钥的情况下解决此问题,您可以覆盖SaveChanges的{​​{1}},但随后使用DbContext以避免访问数据库以查找孤立对象

首先将导航属性添加到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);
    }
}

我做了一次简单的速度测试迭代10.000次。启用modelBuilder.Entity<ParentObject>().HasMany(p => p.ChildObjects) .WithRequired(c => c.ParentObject) .WillCascadeOnDelete(); 后,完成时间为1:01.443分钟,禁用时为0:59.326分钟(均为三次运行的平均值)。测试代码如下。

HandleOrphans()

答案 2 :(得分:1)

想分享另一个对我有用的.net ef核心解决方案,也许有人会觉得它有用。

我有一个带有两个外键(一个或两个)的子表,所以公认的解决方案对我不起作用。根据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);
  }

答案 3 :(得分:1)

在EF Core中,可以通过Delete Orphans完成。

赞:

dbContext.Children.Clear();

答案 4 :(得分:1)

是的。以下在 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() 应该删除所有孤立的子实体。

答案 5 :(得分:0)

这是我针对 Entity Framework 6.4.4 的通用解决方案,不了解特定架构。

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

该方法背后的逻辑是,从所需关系的集合中删除的实体将通过实体框架将其外键更新为 null。因此,我们搜索所有修改后的实体,这些实体至少与具有多重性 'One' 的结尾有一种关系,但外键设置为 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();
  }
}

答案 6 :(得分:-2)

这不是EF现在自动支持的内容。您可以通过在上下文中覆盖SaveChanges并手动删除不再具有父级的子对象来完成此操作。代码将是这样的:

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

    return base.SaveChanges();
}