模型与其自身具有可选关系
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约束。
像往常一样,当你不允许使用级联删除选项(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...
}
只是作为@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
),但是在单独的命令中。
在事务中执行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();
}
}