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

时间:2018-07-11 09:08:33

标签: c# database-design entity-framework-core ef-migrations ef-core-2.0

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

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

我想要的行为是,当我删除Product时,关联的ProductPropertyOptionForProducts也被删除了,但不是这样,与之相关的ProductPropertyOption也没有删除。 ProductPropertyOptionForProducts

这是迁移输出错误消息:

  

在表'PropertyOptionsForProducts'上引入FOREIGN KEY约束'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
);

课程:

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

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

Db schema

2 个答案:

答案 0 :(得分:2)

关系图清楚地显示了从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而不是“普通”时:

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

答案 1 :(得分:2)

“关系”动作被afaik包含在关系数据库的最初设计中。最初,它被视为控制孤立记录可能性的便捷方法。最初是。

但是,随着这些数据库越来越大,Cascade引发了更多的问题,那么它们值得……如您所见。

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

例如,视图“ 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;

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

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

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

delete from ProductForDelete where ID = 300;

一切都会按预期进行。

我们不只是模拟“层叠”功能吗?不,有一个非常重要的区别。如果您已使用级联删除定义了所有表,则从ProductType表中删除将锁定该表,然后锁定Product和ProductProperty表,然后依次进行。必须先锁定每个关系分支中的每个表,然后才能执行任何删除操作。使用视图,首先在链的末尾执行锁定,执行删除操作,释放锁定,然后锁定下一个表。这正是您想要的行为。