ef core - 两个一对一的主键

Flo*_*ian 3 entity-relationship entity-framework

得到以下数据模型:

class EntityA
{
    Guid Id { get; set; }

    //Property1 and Property2 will never be the same
    EntityB Property1 { get; set; }
    EntityB Property2 { get; set; }
}

class EntityB
{
    int Id { get; set; }

    EntityA EntityAProperty { get; set; }
}
Run Code Online (Sandbox Code Playgroud)

但我无法配置关系。EntityA 引用了两个不同的 EntityB。请给我一些关于如何配置它的建议。

尝试过类似的东西(对于 property1 和 property2):

e.HasOne(x => x.Property1)
                    .WithOne()
                    .HasForeignKey<EntityB>(x => x.Property1Id)
                    .IsRequired(false);
Run Code Online (Sandbox Code Playgroud)

或者

e.HasOne(x => x.Property1)
                    .WithOne(x => x.EntityB)
                    .HasForeignKey<EntityB>(x => x.Property1Id)
                    .IsRequired(false);
Run Code Online (Sandbox Code Playgroud)

第一个告诉我

外键属性 {'Id' : int} 的最佳匹配与主键 {'Id' : Guid} 不兼容。

第二个告诉我我不能将属性 EntityB 用于两个关系。

Dia*_*ana 7

您的导航属性有问题EntityB.EntityAProperty。对于每个EntityA记录,EntityB 中将有两条记录在该属性中具有相同的值,因此它不能用作您的一对一 FK,因为 EF 会尝试对其施加唯一约束(在列中)EntityB.EntityAPropertyId)。

我能想到的所有解决方案都需要更改您的模型:

  • 解决方案 1:删除有问题的导航属性EntityB.EntityAProperty。使EntityB您的关系的主要部分,而不是EntityA(根据您的模型的语义,这可能是不可接受的)。这意味着在物理表中您将拥有以下列:

    EntityA: 
        Id (PK)
        Property1Id (FK on EntityB.Id, unique constraint)
        Property2Id (FK on EntityB.Id, unique constraint)
    
    EntityB:
        Id (PK)
    
    Run Code Online (Sandbox Code Playgroud)

    这将不能保证完全参照完整性,您必须在创建/更新EntityA实例时在代码中添加一些检查,特别是检查Property1Id值是否未在另一个实体的 中使用Property2Id,反之亦然。此外,请检查 的同一实例中的两个属性中的值是否不同Entity1

    您的实体的变化:

    class EntityA
    {
        Guid Id { get; set; }
        EntityB Property1 { get; set; }
        EntityB Property2 { get; set; }
        int Property1Id { get; set; }    //New
        int Property2Id { get; set; }    //New
    }
    
    class EntityB
    {
        int Id { get; set; }
        //Removed EntityA property
    }
    
    Run Code Online (Sandbox Code Playgroud)

    中的 fluent API 映射OnModelCreating

    modelBuilder.Entity<EntityA>()
        .HasOne<EntityB>(a => a.Property1)
        .WithOne()    //No navigation property in EntityB
        .HasForeignKey<EntityA>(a => a.Property1Id);
    
    modelBuilder.Entity<EntityA>()
        .HasOne<EntityB>(a => a.Property2)
        .WithOne()    //No navigation property in EntityB
        .HasForeignKey<EntityA>(a => a.Property2Id);
    
    Run Code Online (Sandbox Code Playgroud)
  • 解决方案 2:反过来:主体实体是EntityA,因此 FK 列和唯一约束放在EntityBtable 中。这在语义上似乎是最好的实现,但有一个问题:请记住,由于违反唯一约束(您将有两个具有相同 的记录),您不能仅使用一列EntityB作为 ID 。因此,您必须在 中添加两列,一列用于,一列 用于。由于记录仅同时在这些属性之一中使用,因此每条记录中的两列之一将为空。它看起来确实有点勉强,FK 列很稀疏,50% 的记录中有空值。EntityAEntityA.IdEntityBProperty1Property2EntityB

    这些将是表:

    EntityA: 
        Id (PK)
    
    EntityB:
        Id (PK)
        EntityAProperty1Id (FK on EntityA.Id, unique constraint)
        EntityAProperty2Id (FK on EntityA.Id, unique constraint)
    
    Run Code Online (Sandbox Code Playgroud)

    此解决方案要求您在代码中进行与前一个相同的检查。

    您的实体的变化:

    class EntityA
    {
        Guid Id { get; set; }
        EntityB Property1 { get; set; }
        EntityB Property2 { get; set; }
    }
    
    class EntityB
    {
        int Id { get; set; }
        //Removed EntityA property
        EntityA EntityAProperty1 { get; set; }    //New
        EntityA EntityAProperty2 { get; set; }    //New
        int EntityAProperty1Id { get; set; }    //New
        int EntityAProperty2Id { get; set; }    //New
    }
    
    Run Code Online (Sandbox Code Playgroud)

    中的 fluent API 映射OnModelCreating

    modelBuilder.Entity<EntityB>()
        .HasOne<EntityA>(b => b.EntityAProperty1)
        .WithOne(a => a.Property1)
        .HasForeignKey<EntityB>(b => b.EntityAProperty1Id);
    
    modelBuilder.Entity<EntityB>()
        .HasOne<EntityA>(b => b.EntityAProperty2)
        .WithOne(a => a.Property2)
        .HasForeignKey<EntityB>(b => b.EntityAProperty2Id);
    
    Run Code Online (Sandbox Code Playgroud)
  • 考虑一个不同的解决方案:解决方案 3:放弃你的一对一关系,使用正好有两个值的一对多。不是有两个属性Property1and Property2,而是定义一个集合属性并按位置处理它的值:第一个是Property1,第二个是Property2。我不知道这在功能上是否有意义,也许每个属性的用途完全不同,将它们放在一个列表中没有意义,但以这种方式处理它们会更容易。您可以保留公共属性,并使用底层私有字段,该字段由 EF 映射并保存到数据库表中。您会告诉 EF 忽略公共属性并使用它们的getset使用底层列表的方法。有了这个,您可以继续使用您的导航属性EntityB.EntityAProperty。您仍然需要编写一些代码来检查列表中是否只有两个值。

  • 另一个解决方案,有点偏离主题:解决方案 4:考虑在您的EntityB实体中使用继承。您定义了两个类EntityB1and EntityB2,它们只扩展了父​​类EntityB而不添加任何属性。您EntityA使用派生类型定义属性,而不是父类型:

    class EntityB
    {
        int Id { get; set; }
    }
    
    class EntityB1 : EntityB {}
    
    class EntityB2 : EntityB {}
    
    class EntityA
    {
        Guid Id { get; set; }
        EntityB1 Property1 { get; set; }
        EntityB2 Property2 { get; set; }
    }
    
    Run Code Online (Sandbox Code Playgroud)

    由于Property1Property2具有不同的数据类型,你不再有相同的实体两个关系。EF 应该能够按照惯例弄清楚这一切。您可能不需要添加显式流畅的 API 映射。

我会推荐解决方案 1,但前提是它在语义上EntityB是关系中的主要部分。否则我会推荐解决方案 4。

可以在此处找到有关 EF Core 将一对一关系映射到物理表的方式的更多信息。仅在从属表中添加 FK 列。没有向主体表添加列。与对一对多关系所做的相同,不同之处在于,为了确保关系是一对一而非一对多,还在该 FK 列上创建了唯一约束。

EF 按照惯例为在关系的双方都具有导航属性的两个实体之间的一对一关系执行此操作(在解决方案 2 场景中是这种情况,但不能在解决方案 1 中使用)。

此页面还包含可能与您的问题相关的信息。

对不起,这个答案比我想要的要长。我有点得意忘形了。