簡體   English   中英

EF 可以自動刪除孤立的數據,而父數據沒有被刪除嗎?

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

對於使用 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被刪除。

但是,如果我運行:

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

我得到了例外:

操作失敗:無法更改關系,因為一個或多個外鍵屬性不可為空。 當對關系進行更改時,相關的外鍵屬性將設置為空值。 如果外鍵不支持空值,則必須定義新關系,必須為外鍵屬性分配另一個非空值,或者必須刪除不相關的對象。

這是有道理的,因為實體的定義包括被破壞的外鍵約束。

我可以將實體配置為在它成為孤兒時“自行清除”還是必須從上下文中手動刪除這些ChildObject s(在這種情況下使用 ChildObjectRepository)。

它實際上受支持,但僅當您使用識別關系時才支持。 它也適用於代碼。 你只需要定義復雜的按鍵為您ChildObject同時包含IdParentObjectId

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 分鍾(兩者均是 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();
}

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

像這樣:

dbContext.Children.Clear();

想分享另一個對我有用的 .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);
  }

是的。 以下在 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()應該刪除所有孤立的子實體。

這是我對 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();
  }
}

這不是 EF 現在自動支持的。 您可以通過覆蓋上下文中的 SaveChanges 並手動刪除不再具有父對象的子對象來實現。 代碼將是這樣的:

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