【问题标题】:Entity Framework: Introducing FOREIGN KEY constraint '' on table '' may cause cycles or multiple cascade paths实体框架:在表 '' 上引入 FOREIGN KEY 约束 '' 可能会导致循环或多个级联路径
【发布时间】:2021-04-14 11:50:16
【问题描述】:

尝试使用 Entity Framework 实现在此 answer 上找到的架构时出现错误:

在表“OptionValues”上引入 FOREIGN KEY 约束“FK_OptionValues_Products_ProductId”可能会导致循环或多个级联路径

+---------------+     +---------------+
| PRODUCTS      |-----< PRODUCT_SKUS  |
+---------------+     +---------------+
| #product_id   |     | #product_id   |
|  product_name |     | #sku_id       |
+---------------+     |  sku          |
        |             |  price        |
        |             +---------------+
        |                     |
+-------^-------+      +------^------+
| OPTIONS       |------< SKU_VALUES  |
+---------------+      +-------------+
| #product_id   |      | #product_id |
| #option_id    |      | #sku_id     |
|  option_name  |      | #option_id  |
+---------------+      |  value_id   |
        |              +------v------+
+-------^-------+             |
| OPTION_VALUES |-------------+
+---------------+
| #product_id   |
| #option_id    |
| #value_id     |
|  value_name   |
+---------------+

模型类目前是这样的

public class Option
{
    public int Id { get; set; }

    [ForeignKey("ProductId")]
    public int ProductId { get; set; }

    public string OptionName { get; set; }
    public Product Product { get; set; }
}

public class OptionValue
{
    public int Id { get; set; }

    [ForeignKey("ProductId")]
    public int ProductId { get; set; }

    [ForeignKey("OptionId")]
    public int OptionId { get; set; }

    public string OptionValueName { get; set; }
    public Product Product { get; set; }
    public Option Option { get; set; }
}

public class ProductSku
{
    public int Id { get; set; }

    [ForeignKey("ProductId")]
    public int ProductId { get; set; }

    public string Sku { get; set; }
    public decimal Price { get; set; }
    public Product Product { get; set; }
}

public class SkuValue
{
    public int Id { get; set; }

    [ForeignKey("ProductId")]
    public int ProductId { get; set; }

    [ForeignKey("ProductSkuId")]
    public int ProductSkuId { get; set; }

    [ForeignKey("OptionId")]
    public int OptionId { get; set; }

    [ForeignKey("OptionValueId")]
    public int OptionValueId { get; set; }

    public Product Product { get; set; }
    public ProductSku ProductSku { get; set; }
    public Option Option { get; set; }
    public OptionValue OptionValue { get; set; }
}

我在这里做错了什么?我该如何解决?

【问题讨论】:

标签: c# entity-framework entity-framework-core


【解决方案1】:

这是因为从 OptionValues 中删除一行会从其他表中删除多行。在 MySQL 中,您不应该收到错误,因为我看到这在 SQL Server 中经常发生。尝试添加:

.WillCascadeOnDelete(false);

在你的 modelBuilder 方法上。

这不会级联删除具有外键的所有其他行。

【讨论】:

    【解决方案2】:

    这里有一些相互竞争的东西,您的模型看起来像是在尝试使用多个 FK 来确保完整性,这与默认 EF 实现的一般预期不符,这意味着您将不得不做很多手动操作在您插入和更新单个字段时更新它们,更重要的是 EF 无法自行正确识别关系,您可能需要在 Fluent 配置 中管理更多内容以使其正确.

    以下陈述显示了我如何解释您的模型:

    • Product 有很多“Skus” - ProductSku
    • Product 有很多“选项” - Option
    • Option 有许多命名“值” - OptionValue
    • 每个OptionValue 可以链接到许多“SKU” - SkuValue

    如果是这样,那么你的图表不太正确,你的类定义应该改变。

    总的来说,我会推荐以下内容:

    1. SkuValue 不需要 Product 的 FK,这是从它的父 ProductSku 假定的
    2. OptionValue 也不需要需要Product 的 FK,因为这是从它的父 Option 假定的
    3. SkuValue 不需要 Option 的 FK,因为这是从它的父级 OptionValue 假设的

    通过在每个表中将 FK 留给 Product,EF 感到困惑,因为它可以看到通过在子表中允许这些额外的导航链接,您的代码可以分配一个 不同的 产品对 OptionValue 的说法比在其链接的 Option 中定义的 Product 的说法。

    因此,虽然我们经常认为将字段留在那里有助于维护数据的完整性,但它们通过创建通常不应该存在的链接

    可以保留这些字段,但是当模型存在这样的不一致时,我们需要添加更多配置以使其越界。

    即使进行了这些更改,您仍将在所有这些表之间建立双向链接,如本回复中所述:https://stackoverflow.com/a/65514280/1690217 您将需要删除一个(或全部)默认级联删除行为。


    作为一项规则,我发现删除 Cascade Delete 行为并通过流畅的配置显式管理它会更安全,尤其是当您的子记录具有深度重复链接时。

    只要您坚持简单的模型、只在表中使用单个键、不重复对父记录中定义的子项的键引用并且不形成任何与该表已经是其子项的记录的子关系。

    如果您想删除默认的级联删除行为,我建议您在 OnModelCreating 方法中使用这一行:

    modelBuilder.Conventions.Remove<OneToManyCascadeDeleteConvention>();
    

    如果您尚未删除此约定,那么您将需要使用 Fluent Notation 来配置 ForeignKey,即使您已经尝试使用 Attribute Notation em>

    在 Fluent 表示法中,您可以指定 级联行为 并在单个键上启用或禁用它,外键的 Fluent 配置将覆盖相同键字段的已定义属性。
    Cascade Delete in the EF Core docs

    modelBuilder
         .Entity<Product>()
         .HasMany(p => p.ProductsSkus)
         .WithOne(sku => sku.Product)
         .HasForeignKey(sku => sku.ProductId)
         .OnDelete(DeleteBehavior.Restrict);
    

    关于ForeignKeyAttribute的正确使用

    您对[ForeignKey] 属性的使用不符合要求,因此您必须已经用流利的表示法覆盖了它。注释 Navigation 属性时,使用ForeignKeyAttribute 来描述 FK 属性的名称。也可以反过来使用,可以将属性放在FK属性上,但不用于描述Navigation属性。

    注意细微差别:

    public class Option
    {
        public int Id { get; set; }
        public int ProductId { get; set; }
        public string OptionName { get; set; }
        [ForeignKey("ProductId")]
        public Product Product { get; set; }
    }
    

    或者这样:

    public class Option
    {
        public int Id { get; set; }
        [ForeignKey("Product")]
        public int ProductId { get; set; }
        public string OptionName { get; set; }
        public Product Product { get; set; }
    }
    

    但是下一步,摆脱 magic 字符串并使用对匹配属性的编译时安全引用:

    public class Option
    {
        public int Id { get; set; }
        [ForeignKey(nameof(Option.Product))]
        public int ProductId { get; set; }
        public string OptionName { get; set; }
        public Product Product { get; set; }
    }
    

    通过这种方式,您可以更多地了解传递给 FK 属性的值是对另一个字段的实际引用的重要性。请记住,当使用属性表示法时,当前不支持指定 FK 的级联行为,因此请确保启用或禁用自动应用级联删除的约定,这样您就不必使用 Fluent Notation 定义每个 FK .

    最终,以下类定义(对于您显示的 sn-p)应该可以工作,只需删除任何可能覆盖它的流利表示法:

    请注意,我还定义了来自父记录的导航链接以访问从属记录,这允许您的 Linq 查询在导航链接的任一方向上遍历,而无需使用连接语法。

    public class Product
    {
        public int Id { get; set; }
        ...
        public virtual ICollection<Option> Options { get; set; } = new HashSet<Option>();
        public virtual ICollection<ProductSku> Skus { get; set; } = new HashSet<ProductSku>();
    }
    
    public class Option
    {
        public int Id { get; set; }
        [ForeignKey(nameof(Option.Product))]
        public int ProductId { get; set; }
    
        public string OptionName { get; set; }
        public Product Product { get; set; }
    
        public virtual ICollection<OptionValue> Values { get; set; } = new HashSet<OptionValue>();
    }
    
    public class OptionValue
    {
        public int Id { get; set; }
        [ForeignKey(nameof(OptionValue.Option))]
        public int OptionId { get; set; }
    
        public string OptionValueName { get; set; }
        public Option Option { get; set; }
    
        public virtual ICollection<SkuValue> Skus { get; set; } = new HashSet<SkuValue>();
    }
    
    public class ProductSku
    {
        public int Id { get; set; }
        [ForeignKey(nameof(ProductSku.Product))]
        public int ProductId { get; set; }
    
        public string Sku { get; set; }
        public decimal Price { get; set; }
        public Product Product { get; set; }
    
        public virtual ICollection<SkuValue> Options { get; set; } = new HashSet<SkuValue>();
    }
    
    // many : many link between ProductSku and OptionValue
    public class SkuValue
    {
        public int Id { get; set; }
        [ForeignKey(nameof(SkuValue.ProductSku))]
        public int ProductSkuId { get; set; }
        [ForeignKey(nameof(SkuValue.OptionValue))]
        public int OptionValueId { get; set; }
    
        public ProductSku ProductSku { get; set; }
        public OptionValue OptionValue { get; set; }
    }
    

    【讨论】:

      【解决方案3】:

      尝试从 OptionValue 和 SkuValue 类中移除

       [ForeignKey("ProductId")]
        public int ProductId { get; set; }
      

      因为您已经在 Option 和 ProductSku 类中拥有 ProductId

      【讨论】:

      • 感谢您的努力。现在我在表'SkuValues'上得到'FK_SkuValues_OptionValues_OptionValueId'
      • @IAmNotARobot - 我又做了一次更改,请再试一次,请告诉我。我以为一切都好。
      【解决方案4】:

      这里的问题是如何声明外键。在 SQL 上,您可以将带有“ON UPDATE”和“ON DELETE”参数的外键设置为“CASCADE”或“NO ACTION”。

      “NO ACTION”不会导致该错误,但“CASCADE”会,因为它将与行交互,并且它的所有引用会(或者更好的是)导致 ciclic 引用,例如, OptionsValue 上的删除可能会级联到 Options 中的删除,因此,级联到 Products 中的删除,因为它会导致 ProductsSku 上的删除也会删除 SkusValues(也指产品,所以再次级联...)。

      如果 all (或至少“父”表)设置为“NO ACTION”,则只有在其他表上没有引用该行的情况下才会删除一行,并且可以安全使用。因此,在您的声明中,您需要将该级联选项设置为 false(相当于生成的 sql 语句上的“NO ACTION”)。

      【讨论】:

        猜你喜欢
        • 2011-05-08
        • 2013-10-22
        • 2019-06-14
        • 2020-12-20
        • 2021-05-30
        • 2019-01-18
        相关资源
        最近更新 更多