21

对于使用 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();

所以如果 aParentObject被删除,它的 ChildObjects 也会被删除。

但是,如果我运行:

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

我得到了例外:

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

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

我可以将实体配置为在它成为孤立实体时“自行清理”,还是必须手动ChildObject从上下文中删除这些 s(在本例中使用 ChildObjectRepository)。

4

7 回答 7

33

它实际上受支持,但仅在您使用识别关系时才支持。它也适用于代码。您只需要为ChildObject包含Idand定义复杂的键ParentObjectId

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();
于 2012-05-31T17:18:00.607 回答
4

更新:

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

它基于这篇文章,它使用ObjectStateManager来查找已删除的实体。

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

在这一点上,我找不到任何迹象表明必须删除哪个。与文章的示例相反,在子级具有返回父级的导航属性的情况下,简单地选择第二个会删除父级。因此,为了解决这个问题,我跟踪应该使用 class 处理哪些类型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您的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);
    }
}

您的配置变为:

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

我做了一个简单的速度测试,迭代了 10.000 次。启用它需要 1: 01.443HandleOrphans()分钟才能完成,禁用它是 0:59.326 分钟(两者都是平均 3 次运行)。测试代码如下。

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();
}
于 2017-02-05T04:19:14.553 回答
2

想分享另一个对我有用的 .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);
  }
于 2019-07-01T11:35:49.740 回答
2

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

像这样:

dbContext.Children.Clear();
于 2020-03-03T18:35:48.473 回答
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()应该删除所有孤立的子实体。

于 2021-04-19T14:35:05.763 回答
1

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

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

该方法背后的逻辑是,从所需关系的集合中删除的实体将由实体框架将其外键更新为 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();
  }
}

于 2021-06-30T02:16:52.237 回答
-2

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

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

    return base.SaveChanges();
}
于 2012-05-31T15:26:53.363 回答