只有 1 of Multiple FK 关系约束

Only 1 of Multiple FK relationships Constraint

我有一个合乎逻辑的 table 结构,其中 Table A 与多个其他 table 有关系(Table B & Table C)。但在功能上 Table A can only ever have the FK forBorC` 已填充。

有效Table A记录:

|------|--------|---------|---------|
| ID   | Name   | FK_B    | FK_C    |
|------|--------|---------|---------|
| 1    | Record | -null-  | 3       |
|------|--------|---------|---------|

无效Table A记录:

|------|--------|---------|---------|
| ID   | Name   | FK_B    | FK_C    |
|------|--------|---------|---------|
| 1    | Record | 16      | 3       |
|------|--------|---------|---------|

我想知道如何正确定义此约束,因为 API 与数据库接口的代码并不是唯一的入口。

到目前为止我有以下内容,但它不限制关系的单记录 -null- 要求并允许上述两个示例。

protected override void OnModelCreating(ModelBuilder modelBuilder)
{
   modelBuilder.Entity<TableA>()
       .HasIndex(p => new { p.FK_B, p.FK_C });
}

如何定义此约束以要求仅填充这些 FK 列中的一个而其他列为空?

数据库的所有代码都是 EF Core Code First,使用属性和 Fluent APIs。原始 SQL 解决方案,而 acceptable,在这个项目中将更难管理。所以我正在寻找适合这种约束的解决方案。

您可以通过多种方式将验证合并到您的项目中:

SQL 服务器约束

阅读此线程 dba.stackexchange。com/q/5278/571 如何创建约束。

优点:保证数据一致性的最严格方法

缺点:需要在迁移脚本中自定义 SQL;减慢 DB

建议:如果数据库是集成点,则使用此选项;您无法控制写入它的代码。

应用级别

保存更改验证

EF 有一个方法 ValidateEntity,您可以覆盖它并检查是否两个 FK 都没有设置。 EF Core 没有这个 API。但是,您可以覆盖 SaveChanges 并使用 ChangeTracker 来查找实体并验证它们。

优点:快速简单的解决方案

缺点:不保证持久性级别 (DB) 的一致性

继承

SaveChanges 解决方案很简单,但它仍然允许用户做一些愚蠢的事情并获得运行时验证异常。为了防止这种情况,您可以通过引入 类 的层次结构来强制执行编译类型检查的解决方案: A{Id, Name}, B:Base {FK_B}, C:Base { FK_C}。 EF 和 EF Core 都支持 table-per-hierarchy,因此您的所有实体都将存储在一个 table 中。在您的代码中,您只能向 table 添加 B(使用 FK_B 属性)或 C(使用 FK_C ).

优点:限制您在编译时仅设置 1 个 FK

缺点:更复杂的解决方案,一些操作可能很复杂(例如将实体从 C 转换为 B);查询不那么直接。

建议:如果您在持久性级别上工作并按原样向其他团队公开实体,请稍微控制要保存的内容。

在花了一天时间对此进行研究并通过建议的答案和评论后,我找到了我正在寻找的答案。似乎最新版本的 EFCore 回答了这个问题!

代码优先解决方案

Requires EFCore 3.0

在 EFCore 3.0 中引入了 HasCHeckConstraint 以提供生成 'Check Constraints' 的代码优先解决方案。以下示例说明了 SQL 检查语法界面。有一种语法采用 bool 标志将 DataAnnotations 解释为约束,但此处的文档有限。 Documentation

此解决方案比必须管理 SQL 脚本或手动迁移(如下)要好得多。尽管它仍然依赖于原始 SQL 语句。最终,如果 Fluent API 为此获得一个 CodeGen 方法版本,以便您可以提供 C# 函数而不是 SQL,这将在约束中的字段名称更改时提供更好的处理。

protected override void OnModelCreating(ModelBuilder modelBuilder)
{
   modelBuilder.Entity<TableA>()
       .HasCheckConstraint("CHK_OnlyOneForeignKey", "
           ( CASE WHEN FK_B IS NULL THEN 0 ELSE 1 END
           + CASE WHEN FK_C IS NULL THEN 0 ELSE 1 END
           ) = 1
    ");
}

EFCore 迁移编辑

在所有版本的 EFCore 中,您都可以编辑生成的迁移文件,并在 MigrationBuilder 上使用 Sql(...) 方法来应用任何 SQL 命令。

Note: It is common to create an empty Migration for this purpose.

public partial class CustomCheckConstraint : Migration
{
    protected override void Up(MigrationBuilder migrationBuilder)
    {
        migrationBuilder.Sql("<SQL to alter target table and add a constraint>");
    }

    protected override void Down(MigrationBuilder migrationBuilder)
    {
        // Code to remove Constraint added in Up method
    }
}

Thank you @fenixil and @Ian Mercer for pointing me towards solutions that helped me refine my research.