如何删除级联底部的实体 - > EF中的非级联删除链?

时间:2014-07-18 15:25:06

标签: c# sql entity-framework

对不起这个完全无益的标题......我无法将这个问题变成更好的词语。

我有以下实体,每个实体都由Id属性标识:

  • 底盘

当我使用POCO时,这些实体并没有什么突破性的。例如,Chassis类的定义如下:

public class Chassis
{
    public int Id { get; set; }

    // Other properties omitted for brevity.

    public ICollection<Slot> Slots { get; set; }

    public Chassis() 
    {
        Slots = new Collection<Slot>();
    }
}

关系如下:

  • 机箱有许多插槽(CASCADE)。
  • 一张卡片有多个插槽,因为卡片可以是多插槽卡片(SET NULL)。
  • 广告位必须属于Chassispubic int ChassisId { get; set; }
  • 广告位不必包含Cardpublic int? CardId { get; set; })。

因此,删除机箱时,将删除所有插槽。的即可。但是,我还要删除这些插槽中安装的所有卡。 不太好。我正在尝试将一些代码挂钩到OnSavingChanges事件(我在SaveChanges()之前触发),这样当底盘被标记为已删除时,我也会为它删除卡片。

首先,我试过了:

OnSavingChanges += (x, y) =>
{
    var ctx = x as DbContext;
    var chassis = ctx.ChangeTracker.Entries<Chassis>().Where(e => e.State == EntityState.Deleted);

    // Delete all cards on a deleted chassis.
    foreach (var c in chassis)
    {
        // Cannot just do c.Slots, as EF seems to empty the nav. property now the
        // chassis is deleted.
        var slots = ctx.Slots.Where(s => s.ChassisId == c.Entity.Id).ToList();

        foreach (var s in slots)
        {
            if (s.Card != null)
            {
                ctx.Cards.Remove(s.Card);
            }
        }
    }
};

...但这引发了异常:

  

EntityFramework.dll中发生了'System.Data.Entity.Infrastructure.DbUpdateException'类型的异常,但未在用户代码中处理

     

其他信息:无法插入或更新实体,因为“Chassis_Slots”关系的主要末尾已删除。

然后我尝试在我的内部ctx.Detach(s);循环中添加foreach,以阻止EF尝试保存已删除的via-cascade Slots实体:

foreach (var s in slots)
{
    if (s.Card != null)
    {
        ctx.Cards.Remove(s.Card);
    }

    // Otherwise EF attempts to save the slot, which results in a exception saying the principle
    // end of the relatioship Chassis_Slots has already been deleted.
    ctx.Detach(s);
}

...然而,EF随后会出现以下异常:

  

附加信息:DELETE语句与REFERENCE约束“FK_dbo.Slots_dbo.Cards_CardId”冲突。冲突发生在数据库“...”,表“dbo.Slots”,列'CardId'。

     

声明已经终止。

......这让我在摇滚和硬地之间离开了,完全没有想法。

任何人都可以提出更成功的方法/方法吗?

3 个答案:

答案 0 :(得分:4)

这是序列。

  • 删除Chassis后,由于删除级联机制,所有Slots也将被删除。简单吧?
  • 然后你截取了保存更改,你还想在同一事务Card上手动删除SaveChanges,我不确定EF将如何生成sql查询即使稍后添加了卡删除语法,但是当我使用分析器检查它时,它首先删除了卡(应该是机箱优先)。
  • 会发生什么情况,Unchanged Slot(将delete cascade自动删除)会更改为Modified Slot
  • 为什么?因为当您同时删除Card时,必须将相应的Slot's CardId设置为空。
  • 现在最终结果Slot被修改,但Chassis已被删除。

要解决此问题,您需要引入新的事务/ 上下文。

public override int SaveChanges()
{
    var deletedCardIds = new List<int>();
    var chassises = ChangeTracker.Entries<Chassis>().Where(e => e.State == EntityState.Deleted);
    foreach (var chassis in chassises)
    {
        var slots = Slots.Where(s => s.ChassisId == chassis.Entity.Id).ToArray();
        foreach (var slot in slots)
        {
            if (slot.CardId.HasValue && !deletedCardIds.Contains(slot.CardId.Value))
            {
                deletedCardIds.Add(slot.CardId.Value);
            }
        }
    }

    // Commits original transaction.
    var originalRowsAffected = base.SaveChanges();

    int additionalRowsAffected = 0;
    if (deletedCardIds.Count > 0)
    {
        // Opens new transaction.
        using (var newContext = new AppContext())
        {
            foreach (var cardId in deletedCardIds)
            {
                var deletedCard = newContext.Cards.Find(cardId);
                if (deletedCard != null)
                {
                    newContext.Cards.Remove(deletedCard);
                }
            }

            // Commits new transaction.
            additionalRowsAffected = newContext.SaveChanges();
        }
    }

    return originalRowsAffected + additionalRowsAffected;
}

PS

  • 您有两个独立的交易,可能会导致意外行为(atomicity无法保证)。
  • 您可能希望重新设计数据库以获得理想的解决方案。

更新

今天我意识到我们只需使用TransactionScopecommit several operations in a single transaction。就像在数据库中执行此代码一样。

begin tran
delete from dbo.Chassis
delete from dbo.Cards
commit tran

如果您使用EF6以上,则只能使用Database.BeginTransaction,否则请使用TransactionScope

public override int SaveChanges()
{
    var deletedCardIds = new List<int>();
    var chassises = ChangeTracker.Entries<Chassis>().Where(e => e.State == EntityState.Deleted);
    foreach (var chassis in chassises)
    {
        var cardIds = Slots.Where(s => s.ChassisId == chassis.Entity.Id)
            .Where(s => s.CardId.HasValue)
            .Select(s => s.CardId.Value)
            .ToArray();
        deletedCardIds.AddRange(cardIds);
    }

    int originalRowsAffected;
    int additionalRowsAffected;
    using (var transaction = new TransactionScope())
    {
        originalRowsAffected = base.SaveChanges();

        deletedCardIds.Distinct().ToList()
            .ForEach(id => Entry(new Card { Id = id }).State = EntityState.Deleted);
        additionalRowsAffected = base.SaveChanges();

        transaction.Complete();
    }

    return originalRowsAffected + additionalRowsAffected;
}

通过在上面更新的代码中引入事务,SaveChanges都发生了,或者什么也没发生,现在保证了原子性。

答案 1 :(得分:2)

您当前的解决方案中有一些元素会使级联删除变得麻烦。

  1. Card实体是自我指涉的。即卡可以是叶级卡(没有子卡),也可以是聚合子卡的分支卡。从域的角度来看,我想象一张卡片没有孩子或者只有一组孩子。我将使用这个假定的领域知识作为我的解决方案的一部分。鉴于自由统治,我会将其改为两个实体:CardCardWithChildren,并明确说明这一点。

  2. 在不了解您的域名的情况下,Slot实体似乎纯粹是面向对象的设备,以允许Card个实体属于Chassis或另一个Card {1}}。但是,我会以相反的方式看待问题中所述的关系。而不是一个插槽可以属于一个机箱或一个卡,我会把它建模为一个卡属于一个插槽,然后在卡上多态地表达关系。然后可以进一步减少到只是说卡由机箱或另外的卡拥有。

  3. 这是一个考虑到上述情况的例子。它具有以下特点:

    1. 为了允许卡与包含机箱和父卡之间的关系,我已将机箱和卡类型建模为扩展名为CardContainer的基本类型,该类型通过卡集合聚合卡。

    2. 然后,CardContainer实体模型将被实体框架表示为 TPT (每种类型的表格)策略,从而产生三个表格:CardContainers,{ {1}}和Cards。即我对您的数据模型稍作修改,将卡片和机箱的集合方面抽象为一个单独的表格。然后,ChassisCard实体将共享主键返回到各自的Chassis实体。

    3. 卡表的自我参照性将保留。从理论上讲,这意味着我们可以拥有无​​限深度的卡片 - &gt;卡片关系。但是,要允许实体框架级联删除与TPT策略一起使用,我们需要明确告诉实体框架在检索卡时的最大深度。这是通过包括正确的卡片深度来完成的,作为检索机箱实体的一部分。

    4. 此方法需要在CardContainer上单次调用SaveChanges,因此原子

      注意:由于上述原因,此解决方案不包含Context实体,并且保持代码示例较小(ish)。但是,通过使SlotCard扩展Chassis,可以轻松扩展此解决方案,然后再SlotCollection聚合Slot个实体,其中包含{{1}个实体}} 实体。基本方法保持不变。

      实体框架代码第一个POCO&#39>

      Card

      POCO配置

      public abstract class CardContainer
      {
          public int Id { get; set; }
          public ICollection<Card> Cards { get; set; }
      
          protected CardContainer()
          {
              Cards = new List<Card>();
          }
      }
      
      public class Chassis : CardContainer
      {             
      }
      
      public class Card : CardContainer
      {
          public CardContainer Container { get; set; }
          public int ContainerId { get; set; }
      }    
      

      示例上下文

      public class CardContainerConfiguration : EntityTypeConfiguration<CardContainer>
      {
          public CardContainerConfiguration()
          {
              ToTable("CardContainers");
              HasKey(k => k.Id);
              Property(p => p.Id).HasDatabaseGeneratedOption(DatabaseGeneratedOption.Identity);   
              HasMany(f => f.Cards)
                  .WithRequired(p => p.Container)
                  .HasForeignKey(p => p.ContainerId)
                  .WillCascadeOnDelete(true);
          }
      }
      
      public class ChassisConfiguration : EntityTypeConfiguration<Chassis>
      {
          public ChassisConfiguration()
          {
              ToTable("Chassis");
          }
      }
      
      public class CardConfiguration : EntityTypeConfiguration<Card>
      {
          public CardConfiguration()
          {
              ToTable("Cards");
              HasRequired(x => x.Container).WithMany(x => x.Cards).HasForeignKey(x => x.ContainerId);            
          }
      }    
      

      使用示例

      public class EfContext : DbContext
      {              
          protected override void OnModelCreating(DbModelBuilder modelBuilder)
          {           
              modelBuilder.Configurations.Add(new CardContainerConfiguration());
              modelBuilder.Configurations.Add(new ChassisConfiguration());
              modelBuilder.Configurations.Add(new CardConfiguration());           
      
              base.OnModelCreating(modelBuilder);
          }
      }
      

答案 2 :(得分:1)

为什么要删除机箱,然后仅在额外的事件中删除卡?我相信这会导致你所有的问题。在我的项目中,每当我想要在数据库中删除没有明确级联关系的父级和子级时,我总是从子级开始,然后删除父级而没有任何问题。

在您的代码中删除机箱,获取所有插槽并迭代它们并删除所有卡,就像您在活动中一样。如果在删除机箱或插槽之前将卡标记为已删除,则在删除卡时,它不会将插槽标记为已修改。这应该可以防止您的错误,让您删除一个事务中的所有内容。

它也更清晰,更容易阅读,以便在一个地方标记所有被删除的作品。现在,任何阅读你的机箱删除的人都很容易意识到这也删除了与之相关的任何卡。