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

Sti*_*ian 5 c# database-design entity-framework-core ef-core-2.0 entity-framework-migrations

为什么我最初update-database失败了,我需要在数据库表类中更改什么才能使其工作?

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

我想要的行为是,当我删除 a 时Product,关联的ProductPropertyOptionForProducts也会被删除,但反之则不然,而不是ProductPropertyOption与 关联的ProductPropertyOptionForProducts

这是迁移输出错误消息:

在表“PropertyOptionsForProducts”上引入外键约束“FK_PropertyOptionsForProducts_ProductPropertyOptions_ProductPropertyOptionId”可能会导致循环或多个级联路径。指定 ON DELETE NO ACTION 或 ON UPDATE NO ACTION,或修改其他 FOREIGN KEY 约束。无法创建约束或索引。请参阅以前的错误。

生成的导致错误的 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
);
Run Code Online (Sandbox Code Playgroud)

课程:

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; }
}
Run Code Online (Sandbox Code Playgroud)

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

数据库架构

Iva*_*oev 8

ProductType关系图清楚地显示了从到 的多重级联路径PropertyOptionForProduct

(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);
Run Code Online (Sandbox Code Playgroud)

然后,当您需要删除ProductType, 而不是“正常”时:

db.Remove(db.Set<ProductType>().Single(e => e.Id == id));
db.SaveChanges();
Run Code Online (Sandbox Code Playgroud)

您必须先删除相关的Properties

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


Tom*_*att 5

据我所知,“级联”操作包含在关系数据库的最初设计中。一开始,它被视为控制孤立记录可能性的一种便捷方法。起初是这样的。

然而,随着这些数据库变得越来越大,级联造成的问题比它们的价值还要多……正如您所看到的。

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

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

select * from ProductTypeForDelete where ID = 1001;
ID    TABLE              KEY
===== ==========         =====
1001  Product            300
1001  Product            301
1001  ProductProperty    203
Run Code Online (Sandbox Code Playgroud)

考虑以下命令:

delete from ProductTypeForDelete where ID = 1001;
Run Code Online (Sandbox Code Playgroud)

触发器将收到上面显示的结果集。它在 Product 表中显示 2 个依赖项,在 ProductProperty 表中显示 1 个依赖项。因此delete视图上的触发器知道它需要先从这两个表中删除,然后再从 ProductType 表中删除。

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

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

delete from ProductForDelete where ID = 300;
Run Code Online (Sandbox Code Playgroud)

一切都按预期进行。

我们不就是在模仿“级联”特性吗?不,有一个非常重要的区别。如果您已经使用级联删除定义了所有表,则从 ProductType 表中删除将锁定该表,然后锁定 Product 和 ProductProperty 表,并继续下去。在执行任何删除之前,必须锁定每个关系分支中的每个表。使用视图,首先在链的末尾执行锁定,执行删除,释放锁,然后锁定下一个表。这正是您想要的行为。