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)。
它实际上是受支持的,但仅当您使用标识关系时。它也适用于代码。您只需要为包含Id
和ParentObjectId
的ChildObject
定义复杂键:
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; }
}
原始答案
要在不设置复杂键的情况下解决此问题,您可以覆盖DbContext
的SaveChanges
,但随后使用 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();
}