EF Core 删除相同 table 上的一对一关系

EF Core delete one-to-one relation on same table

模型与自身有可选关系

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: The DELETE statement conflicted with the SAME TABLE REFERENCE constraint "FK_Item_Item_OptionalItemId". The conflict occurred in database "local-database", table "dbo.Item", column 'OptionalItemId'.

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

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

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

Introducing FOREIGN KEY constraint 'FK_Item_Item_OptionalItemId' on table 'Item' may cause cycles or multiple cascade paths.
Specify ON DELETE NO ACTION or ON UPDATE NO ACTION, or modify other FOREIGN KEY constraints.

像往常一样,当您不允许使用级联删除选项时(顺便说一句,SqlServer 限制,某些数据库如 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...  
}

仅作为@Ivan 回答的补充。

Item 有一个外键 OptionalItem,这意味着 Item 依赖于 OptionalItem

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

EF Core 支持从主体到依赖的 "cascade deletes"。正如 Ivan Stoev 提到的,迁移期间的异常是 Sql 服务器限制。但是EF Core还是会支持的,你可以试试
- 添加 .OnDelete(DeleteBehavior.Cascade)
- 运行 dotnet ef migrations add <migration-name>
- 通过删除 CASCADE 操作更新生成的迁移脚本
- 使用刚刚创建的迁移更新数据库

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

因此,在您的情况下,您可以尝试在依赖 Item 之前删除主要项目 (OptionalItem),但在单独的命令中。
在事务中全部执行,出错时回滚

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();         
    }
}