FK 约束可能会导致循环或多个级联路径

FK constraints may cause cycles or multiple cascade paths

为什么我的初始 update-database 失败了,我需要在我的数据库 table class(es) 中更改什么才能使其正常工作?

当然,我可以将迁移脚本中的 onDelete: ReferentialAction.Cascade 更改为 onDelete: ReferentialAction.NoAction,但这样我的应用程序就会面临其他问题。我正在寻求一种无需编辑 add-migration 生成的迁移脚本的解决方案。换句话说,我愿意更改我的数据库架构。

我想要的行为是,当我删除 Product 时,关联的 ProductPropertyOptionForProducts 也会被删除,但反过来不会,而不是关联的 ProductPropertyOptionProductPropertyOptionForProducts.

这是迁移输出错误信息:

Introducing FOREIGN KEY constraint 'FK_PropertyOptionsForProducts_ProductPropertyOptions_ProductPropertyOptionId' on table 'PropertyOptionsForProducts' may cause cycles or multiple cascade paths. Specify ON DELETE NO ACTION or ON UPDATE NO ACTION, or modify other FOREIGN KEY constraints. Could not create constraint or index. See previous errors.

导致错误的生成的SQL命令:

CREATE TABLE[PropertyOptionsForProducts] (
[Id] int NOT NULL IDENTITY,
[CustomNumberValue] decimal (18, 2) NOT NULL,
[CustomRangeFrom] decimal (18, 2) NOT NULL,
[CustomRangeTo] decimal (18, 2) NOT NULL,
[CustomStringValue] nvarchar(max) NULL,
[ProductId] int NOT NULL,
[ProductPropertyId] int NOT NULL,
[ProductPropertyOptionId] int NOT NULL,
CONSTRAINT[PK_PropertyOptionsForProducts] PRIMARY KEY([Id]),
CONSTRAINT[FK_PropertyOptionsForProducts_Products_ProductId]
    FOREIGN KEY([ProductId])
    REFERENCES[Products] ([Id]) ON DELETE CASCADE,
CONSTRAINT[FK_PropertyOptionsForProducts_ProductPropertyOptions_ProductPropertyOptionId]
    FOREIGN KEY([ProductPropertyOptionId])
    REFERENCES[ProductPropertyOptions] ([Id]) ON DELETE CASCADE
);

class是:

public class ProductPropertyOption
{
    public int Id { get; set; }
    public int ProductPropertyId { get; set; }
    // some more properties
    public ProductProperty Property { get; set; }
    public ICollection<PropertyOptionForProduct> PropertyOptionForProducts { get; set; }
}


public class PropertyOptionForProduct
{
    public int Id { get; set; }
    public int ProductId { get; set; }
    public int ProductPropertyId { get; set; }
    public int ProductPropertyOptionId { get; set; }
    // some more properties
    public Product Product { get; set; }
    public ProductPropertyOption ProductPropertyOption { get; set; }
}


public class Product
{
    public int Id { get; set; }
    public bool Published { get; set; }
    public int ProductGroupId { get; set; }
    public int ProductGroupSortOrder { get; set; }
    // some more properties
    public int ProductTypeId { get; set; }

    public ICollection<ProductImage> Images { get; set; }
    public ICollection<PropertyOptionForProduct> ProductPropertyOptionForProducts { get; set; }
    public ICollection<IdentifierForProduct> IdentifierForProducts { get; set; }
    public ProductType Type { get; set; }
    public ICollection<FrontPageProduct> InFrontPages { get; set; }
    public ICollection<ProductInCategory> InCategories { get; set; }
}


public class ProductType
{
    public int Id { get; set; }
    public string Title { get; set; }
    public List<ProductIdentifierInType> Identifiers { get; set; }
    public List<ProductProperty> Properties { get; set; }
    public ICollection<Product> Products { get; set; }
}


public class ProductProperty
{
    public int Id { get; set; }
    public int ProductTypeId { get; set; }
    // some more properties
    public List<ProductPropertyOption> Options { get; set; }
    public ProductType ProductType { get; set; }
}

说明的数据库(产品和类别部分):

关系图清楚地显示了从ProductTypePropertyOptionForProduct的多重级联路径:

(1) ProductType -> Product -> PropertyOptionForProduct

(2) ProductType -> ProductProperty -> ProductPropertyOption -> PropertyOptionForProduct

唯一的解决办法是通过关闭至少一个关系的级联删除来打破级联路径,然后手动处理主体实体删除。

可能最简单的是破坏一些根路径,例如 ProductType -> ProductProperty:

modelBuilder.Entity<ProductType>()
    .HasMany(e => e.Properties)
    .WithOne(e => e.ProductType)
    .OnDelete(DeleteBehavior.Restrict);

然后当你需要删除一个ProductType,而不是"normal":

db.Remove(db.Set<ProductType>().Single(e => e.Id == id));
db.SaveChanges();

你必须先删除相关的 Properties:

var productType = db.Set<ProductType>().Include(e => e.Properties).Single(e => e.Id == id);
db.RemoveRange(productType.Properties);
db.Remove(productType);
db.SaveChanges();

据我所知,"cascade" 操作包含在关系数据库的最初设计中。一开始,它被视为控制孤立记录可能性的一种便捷方式。而且是……一开始。

然而,随着这些数据库变得越来越大,Cascade 造成了更多的问题,以至于它们的价值……如您所见。

一种解决方案是创建扩展所有直接关系的视图。视图上的 "instead of" 触发器将在删除目标实体之前处理依赖实体的删除。

例如,视图 "ProductTypeForDelete" 可能如下所示:

select * from ProductTypeForDelete where ID = 1001;
ID    TABLE              KEY
===== ==========         =====
1001  Product            300
1001  Product            301
1001  ProductProperty    203

考虑命令:

delete from ProductTypeForDelete where ID = 1001;

触发器将收到如上所示的结果集。它在产品 table 中显示了 2 个依赖项,在 ProductProperty table 中显示了一个。因此视图上的 delete 触发器知道它需要在从 ProductType table.

中删除之前从这两个 table 中删除

还会有继续链的 ProductForDelete 和 ProductPropertyForDelete 视图。视图 PropertyOptionForProductForDelete 上的 delete 触发器会知道它位于链的末尾并执行删除。然后执行链将展开,从他们的目标 table 中删除。

您可能认为会有很多视图和很多触发器,但它只是代码而且非常容易维护。另一个优点是这在从关系链中的任何位置删除时都有效。要删除产品而不是整个产品类型,只需发出命令:

delete from ProductForDelete where ID = 300;

一切都按预期进行。

我们不就是在模仿"cascade"的功能吗?不,有一个很重要的区别。如果您已使用级联删除定义了所有 tables,从 ProductType table 中删除将锁定 table,然后锁定 Product 和 ProductProperty tables 等等线。在执行任何删除之前,必须锁定每个关系分支中的每个 table。使用视图,首先在链的末尾执行锁定,执行删除,释放锁然后锁定下一个 table 。这正是您想要的行为。

您可以将此添加到 DataContext.cs,这对我有用。

protected override void OnModelCreating(DbModelBuilder modelBuilder)
{
   modelBuilder.Conventions.Remove<OneToManyCascadeDeleteConvention>();
   modelBuilder.Conventions.Remove<ManyToManyCascadeDeleteConvention>();
}