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

问题描述 投票:3回答:2

模型与其自身具有可选关系

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 REFERENCE约束“FK_Item_Item_OptionalItemId”冲突。冲突发生在数据库“local-database”,表“dbo.Item”,列“OptionalItemId”中。

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

我尝试过更改删除行为 - .OnDelete(DeleteBehavior.Cascade) 和 - .OnDelete(DeleteBehavior.SetNull) 但是在应用迁移到数据库期间,这两种行为都会抛出异常。

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

c# sql-server .net-core entity-framework-core ef-core-2.1
2个回答
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
投票

只是作为@Ivan的答案的补充。

ItemOptionalItem的外键,这意味着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),但是在单独的命令中。 在事务中执行all,因此在发生错误时将回滚操作。

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();         
    }
}
© www.soinside.com 2019 - 2024. All rights reserved.