实体框架:在表 '' 上引入 FOREIGN KEY 约束 '' 可能会导致循环或多个级联路径

时间:2021-01-08 18:37:27

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

尝试使用实体框架实现在此 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; }
}

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

4 个答案:

答案 0 :(得分:2)

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

<块引用>

.WillCascadeOnDelete(false);

关于您的模型构建器方法。

这不会使用外键级联删除所有其他行。

答案 1 :(得分:1)

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

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

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

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

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

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

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

<块引用>

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

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

<块引用>

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


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

<块引用>

EF 中的默认约定工作得相当好,只要您坚持使用简单模型,只在表中使用单个键,不要重复对父记录中定义的子项的键引用,并且不形成任何与该表已经是其子项的记录的子关系。

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

modelBuilder.Conventions.Remove<OneToManyCascadeDeleteConvention>();

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

<块引用>

在 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 .

最终,以下类定义(对于您显示的代码段)应该可以工作,只需删除任何可能覆盖此的流畅符号:

请注意,我还定义了来自父记录的导航链接以访问从属记录,这允许您的 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; }
}

答案 2 :(得分:0)

尝试从 OptionValue 和 SkuValue 类中删除

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

因为您已经在 Option 和 ProductSku 类中设置了 ProductId

答案 3 :(得分:0)

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

“NO ACTION”不会导致该错误,但“CASCADE”会,因为它会与行及其所有会(或更好,可能)导致 ciclic 引用的引用交互,例如,在 OptionsValue 上删除可能会级联到选项中的删除,因此,级联到产品中的删除,因为它会导致 ProductsSku 上的删除也会删除 SkusValues(也指产品,因此再次级联......)。 /p>

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