EF Core删除同一表上的一对一关系

时间:2018-09-02 11:23:16

标签: c# sql-server .net-core entity-framework-core ef-core-2.1

模型本身具有可选关系

public class Item
{
    public Guid Id { get; set; }
    public string Description { get; set; }
    public Guid StockId { get; set; }

    // optionally reference to another item from different stock
    public Guid? OptionalItemId { get; set; }

    public virtual Item OptionalItem { get; set; }      
}

在DbContext模型中,其配置如下:

protected override void OnModelCreating(ModelBuilder builder)
{
     builder.Entity<Item>().HasOne(item => item.OptionalItem)
                           .WithOne()
                           .HasForeignKey<Item>(item => item.OptionalItemId)
                           .HasPrincipalKey<Item>(item => item.Id)
                           .IsRequired(false)
}

我想通过在更新新Stock之前删除现有项来用新项替换现有项。

// Given Stock contains only new items
public void Update(Stock stock)
{
    using (var context = CreateContext())
    {
        // Remove old items
        var oldItems = context.Items
                              .Where(item => item.StockId == stock.Id)
                              .Select(item => new Item { Id = item.Id })
                              .ToList();
        context.Items.RemoveRange(oldItems);

        // Remove optional items from another stock
        var oldOptionalItems = context.Items
                                      .Where(item => item.StockId == stock.RelatedStock.Id)
                                      .Select(item => new Item { Id = item.Id })
                                      .ToList();
        context.Items.RemoveRange(oldOptionalItems);   

        context.Stocks.Update(stock);
        context.SaveChanges();         
    }
}

问题是执行Update方法时,第context.SaveChanges()行会引发异常:

  

SqlException:DELETE语句与SAME TABLE冲突   参考约束   “ FK_Item_Item_OptionalItemId”。的   表“数据库“本地数据库”中发生冲突   “ dbo.Item”,“ OptionalItemId”列。

我发现了另一个具有类似问题的问题:The DELETE statement conflicted with the SAME TABLE REFERENCE constraint with Entity Framework
但是看起来所有答案都与实体框架(不是EF Core)有关。

我尝试将删除行为更改为
-.OnDelete(DeleteBehavior.Cascade)

-.OnDelete(DeleteBehavior.SetNull)
但是在将迁移应用于数据库期间,这两种行为都会在下面引发异常。

  

在上引入FOREIGN KEY约束'FK_Item_Item_OptionalItemId'   表“项目”可能会导致循环或多个级联路径。
  指定ON DELETE NO ACTION或ON UPDATE NO ACTION,或修改其他FOREIGN KEY约束。

2 个答案:

答案 0 :(得分:3)

通常,当不允许使用级联删除选项时(SqlServer限制btw,某些数据库(如Oracle)没有此类问题),需要在删除记录之前(递归)删除相关数据。

它可以一个接一个地执行,也可以按级别完成(较少的SQL命令,但可能使用大的IN PK列表)。还可以使用基于CTE的SQL确定相关数据-这是最有效的数据库不可知方式。

以下方法实现了第二种方法:

static void DeleteItems(DbContext context, Expression<Func<Item, bool>> filter)
{
    var items = context.Set<Item>().Where(filter).ToList();
    if (items.Count == 0) return;
    var itemIds = items.Select(e => e.Id);
    DeleteItems(context, e => e.OptionalItemId != null && itemIds.Contains(e.OptionalItemId.Value));
    context.RemoveRange(items);
}

,可以在您的代码中这样使用:

using (var context = CreateContext())
{
    // Remove old items
    DeleteItems(context, item => item.StockId == stock.Id);

    // Remove optional items from another stock
    DeleteItems(context, item => item.StockId == stock.RelatedStock.Id);

    // The rest...  
}

答案 1 :(得分:1)

仅作为@Ivan答案的补充。

Item的外键为OptionalItem,这意味着Item依赖于OptionalItem

`Item`(dependent) -> `OptionalItem`(principal)

EF Core支持从主体到依赖项的“级联删除”。如Ivan Stoev所述,迁移期间的异常是Sql Server的限制。但是EF Core仍会支持它,您可以尝试
 -添加.OnDelete(DeleteBehavior.Cascade)
 -运行dotnet ef migrations add <migration-name>
 -通过删除CASCADE操作来更新生成的迁移脚本
 -使用刚创建的迁移更新数据库

在将迁移应用于数据库期间,您不会遇到异常。
注意:
  1.(再次)EF Core支持从主体到从属的级联删除
相关的Item将在您删除OptionalItem的记录时被删除
  2. EF Core将仅自动删除DbContext已跟踪的相关记录(已加载到内存中)

因此,在您的情况下,您可以尝试删除相关项OptionalItem之前的主体项目(Item),但要使用单独的命令。
在事务中执行所有操作,因此发生错误时将回滚操作。

public void Update(Stock stock)
{
    using (var context = CreateContext())
    using (var transaction = context.Database.BeginTransaction())
    {
        // Remove optional items from another stock
        // This is principal record in the items relation
        var oldOptionalItems = context.Items
                                      .Where(item => item.StockId == stock.RelatedStock.Id)
                                      .Select(item => new Item { Id = item.Id })
                                      .ToList();
        context.Items.RemoveRange(oldOptionalItems);

        // Remove them actually from the database
        context.SaveChanges();

        // Remove old items
        var oldItems = context.Items
                          .Where(item => item.StockId == stock.Id)
                          .Select(item => new Item { Id = item.Id })
                          .ToList();
        context.Items.RemoveRange(oldItems);

        context.Stocks.Update(stock);
        context.SaveChanges();         
    }
}