简体   繁体   English

EF 可以自动删除孤立的数据,而父数据没有被删除吗?

[英]Can EF automatically delete data that is orphaned, where the parent is not deleted?

For an application using Code First EF 5 beta I have:对于使用 Code First EF 5 beta 的应用程序,我有:

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

and

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

The relevant CRUD operations are performed by repositories, where necessary.必要时,相关 CRUD 操作由存储库执行。

In

OnModelCreating(DbModelBuilder modelBuilder)

I have set them up:我已经设置了它们:

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

So if a ParentObject is deleted, its ChildObjects are too.因此,如果ParentObject被删除,它的 ChildObjects ParentObject被删除。

However, if I run:但是,如果我运行:

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

I get the exception:我得到了例外:

The operation failed: The relationship could not be changed because one or more of the foreign-key properties is non-nullable.操作失败:无法更改关系,因为一个或多个外键属性不可为空。 When a change is made to a relationship, the related foreign-key property is set to a null value.当对关系进行更改时,相关的外键属性将设置为空值。 If the foreign-key does not support null values, a new relationship must be defined, the foreign-key property must be assigned another non-null value, or the unrelated object must be deleted.如果外键不支持空值,则必须定义新关系,必须为外键属性分配另一个非空值,或者必须删除不相关的对象。

This makes sense as the definition of the entities includes the foreign key constraint which is being broken.这是有道理的,因为实体的定义包括被破坏的外键约束。

Can I configure the entity to "clear itself up" when it gets orphaned or must I manually remove these ChildObject s from the context (in this case using a ChildObjectRepository).我可以将实体配置为在它成为孤儿时“自行清除”还是必须从上下文中手动删除这些ChildObject s(在这种情况下使用 ChildObjectRepository)。

It is actually supported but only when you use Identifying relation .它实际上受支持,但仅当您使用识别关系时才支持。 It works with code first as well.它也适用于代码。 You just need to define complex key for your ChildObject containing both Id and ParentObjectId :你只需要定义复杂的按键为您ChildObject同时包含IdParentObjectId

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

Because defining such key will remove default convention for auto incremented Id you must redefine it manually:因为定义这样的键将删除自动递增 Id 的默认约定,所以您必须手动重新定义它:

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

Now calling to parentObject.ChildObjects.Clear() deletes dependent objects.现在调用 parentObject.ChildObjects.Clear() 删除依赖对象。

Btw.顺便提一句。 your relation mapping should use WithRequired to follow your real classes because if FK is not nullable, it is not optional:您的关系映射应该使用WithRequired来跟随您的真实类,因为如果 FK 不可为空,则它不是可选的:

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

Update:更新:

I found a way that doesn't need to add navigational properties from the child to the parent entity or to set up a complex key.我找到了一种不需要将导航属性从子实体添加到父实体或设置复杂键的方法。

It's based on this article which uses the ObjectStateManager to find the deleted entities.它基于这篇文章,它使用ObjectStateManager来查找已删除的实体。

With a list ObjectStateEntry in hand, we can find a pair of EntityKey from each, which represents the relationship that was deleted.手头有一个列表ObjectStateEntry ,我们可以从每个列表中找到一对EntityKey ,代表被删除的关系。

At this point, I couldn't find any indication of which one had to be deleted.在这一点上,我找不到任何指示必须删除哪个。 And contrary to the article's example, simply picking the second one would get the parent deleted in cases where the child had a navigation property back to the parent.与文章的示例相反,如果子级具有返回父级的导航属性,只需选择第二个就会删除父级。 So, in order to fix that, I track which types should be handled with the class OrphansToHandle .所以,为了解决这个问题,我跟踪哪些类型应该用类OrphansToHandle处理。

The Model:该模型:

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

The other classes:其他班级:

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

Original Answer原答案

To solve this problem without setting up a complex key, you can override the SaveChanges of your DbContext , but then use ChangeTracker to avoid accessing the database in order to find orphan objects.要在不设置复杂键的情况下解决此问题,您可以覆盖DbContextSaveChanges ,然后使用ChangeTracker避免访问数据库以查找孤立对象。

First add a navigation property to the ChildObject (you can keep int ParentObjectId property if you want, it works either way):首先向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; }
}

Then look for orphan objects using ChangeTracker :然后使用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);
    }
}

Your configuration becomes:您的配置变为:

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

I did a simple speed test iterating 10.000 times.我做了一个简单的速度测试,迭代了 10.000 次。 With HandleOrphans() enabled it took 1:01.443 min to complete, with it disabled it was 0:59.326 min (both are an average of three runs).启用HandleOrphans()需要 1:01.443 分钟才能完成,禁用时则为 0:59.326 分钟(两者均是 3 次运行的平均值)。 Test code below.测试代码如下。

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

In EF Core, it can be done by Delete Orphans .在 EF Core 中,可以通过Delete Orphans来完成。

Like this:像这样:

dbContext.Children.Clear();

Want to share another .net ef core solution that worked for me, may be somebody will find it usefull.想分享另一个对我有用的 .net ef 核心解决方案,可能有人会发现它很有用。

I had a child table with two foreign keys (either or), so the accepted solution didn't work for me.我有一个带有两个外键(或)的子表,所以接受的解决方案对我不起作用。 Based on the answer by Marcos Dimitrio I came up with the following:根据 Marcos Dimitrio 的回答,我想出了以下内容:

In my custom DbContext:在我的自定义 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);
  }

Yes.是的。 The following works in EF Core:以下在 EF Core 中有效:

Make sure you set the cascade behavior to Cascade like so:确保将级联行为设置为Cascade如下所示:

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

Then set the Parent property to be null in all the child entities that are to be deleted like so:然后在所有要删除的子实体中将Parent属性设置为 null,如下所示:

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

Now, context.SaveAsync() should delete all the orphaned children entities.现在, context.SaveAsync()应该删除所有孤立的子实体。

This is my generic solution for Entity Framework 6.4.4, without knowledge of the particular schema.这是我对 Entity Framework 6.4.4 的通用解决方案,不了解特定架构。

Note that I start my search for orphan entities from modified entity entries, as in my case I could not find anything searching for deleted relationship entries like other answers suggest.请注意,我从修改后的实体条目开始搜索孤立实体,因为在我的情况下,我找不到任何像其他答案建议的那样搜索已删除关系条目的内容。

The logic behind the approach is that an entity removed from a collection of a required relationship will have its foreign key updated to null by the Entity Framework.该方法背后的逻辑是,从所需关系的集合中删除的实体将通过实体框架将其外键更新为 null。 So we search for all modified entities which have at least one relationship to an end with multiplicity 'One' but having the foreign key set to null.因此,我们搜索所有修改后的实体,这些实体至少与具有多重性 'One' 的结尾有一种关系,但外键设置为 null。

Add this method to your DbContext subclass.将此方法添加到您的DbContext子类。 You could override the SaveChanges / SaveChangesAsync methods to call this method automatically.您可以覆盖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();
  }
}

This is not something that is supported automatically by EF right now.这不是 EF 现在自动支持的。 You can do it by overriding SaveChanges in your context and manually deleting an child objects that no longer have a parent.您可以通过覆盖上下文中的 SaveChanges 并手动删除不再具有父对象的子对象来实现。 The code would be something like this:代码将是这样的:

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

    return base.SaveChanges();
}

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

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