ef core - two one to one on one principal key

a 夏天 提交于 2020-07-09 13:23:48

问题


Got the following datamodel:

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

But I can't manage to configure the relation. EntityA references two different EntityB. Please give me some advise on how to configure it.

Tried something like (for property1 and property2):

e.HasOne(x => x.Property1)
                    .WithOne()
                    .HasForeignKey<EntityB>(x => x.Property1Id)
                    .IsRequired(false);

or

e.HasOne(x => x.Property1)
                    .WithOne(x => x.EntityB)
                    .HasForeignKey<EntityB>(x => x.Property1Id)
                    .IsRequired(false);

First one tells me that

The best match for foreign key properties {'Id' : int} are incompatible with the principal key {'Id' : Guid}.

The second one tells me that I can not use the property EntityB for two relations.


回答1:


There is a problem with your navigation property EntityB.EntityAProperty. For each record in EntityA there will be two records in EntityB with the same value in that property, so it can't be used as your one-to-one FK, as EF would try to put a unique constraint on it (in the column EntityB.EntityAPropertyId).

All the solutions I can think of require changing your model:

  • Solution 1: Remove the problematic navigation property EntityB.EntityAProperty. Make EntityB the principal part of your relationships, instead of EntityA (perhaps this is not acceptable according to the semantics of your model). This means that in the physical tables you will have these columns:

    EntityA: 
        Id (PK)
        Property1Id (FK on EntityB.Id, unique constraint)
        Property2Id (FK on EntityB.Id, unique constraint)
    
    EntityB:
        Id (PK)
    

    This will not guarantee full referential integrity, you will have to add some checkings in code when creating/updating EntityA instances, specifically checking that a Property1Id value is not used in another entity's Property2Id, and the other way around. Also, check that the value is not the same in both properties in the same instance of Entity1.

    The changes in your entities:

    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
    }
    

    The fluent API mappings in 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);
    
  • Solution 2: The other way around: the principal entity is EntityA, so the FK columns and unique constraints are placed in EntityB table. This seems the best implementation semantically, but has a problem: remember that you can't use only one column in EntityB for the ID in EntityA because of the unique constraint violation (you would have two records with the same EntityA.Id). So you would have to add two columns in EntityB, one for Property1and one for Property2. As a record in EntityB is used only in one of those properties at the same time, then one of the two columns would be null in each record. It really looks a little forced and the FK columns are sparse, with null value in 50% of the records.

    These would be the tables:

    EntityA: 
        Id (PK)
    
    EntityB:
        Id (PK)
        EntityAProperty1Id (FK on EntityA.Id, unique constraint)
        EntityAProperty2Id (FK on EntityA.Id, unique constraint)
    

    This solution requires you to do the same checkings in code than the previous one.

    The changes in your entities:

    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
    }
    

    The fluent API mappings in 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);
    
  • Consider a different solution: Solution 3: Give up on your one-to-one relationships and use a one-to-many with exactly two values. Instead of having two properties Property1 and Property2, define a collection property and deal with its values by position: the first one is Property1, the second one is Property2. I don't know if this makes sense functionally, perhaps the uses of each property are totally different and putting them together in a list doesn't make sense, but it would be easier to deal with them this way. You can keep the public properties as they are, and use an underlying private field that is the one that gets mapped by EF and saved to the database tables. You would tell EF to ignore the public properties and use their get and set methods to use the undelying list. With this you can keep using your navigation property EntityB.EntityAProperty. You still would have to write some code to check that there are only two values in the list.

  • Another solution, a bit off-topic: Solution 4: Consider using inheritance in your EntityB entity. You define two classes EntityB1 and EntityB2, that only extend the parent EntityB without adding any property. You define the properties in EntityA using the derived types, not the parent one:

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

    As Property1 and Property2 have different data types you no longer have two relationships with the same entity. EF should be able to figure it all out by convention. Probably you won't need to add explicit fluent API mappings.

I would recommend solution 1, but only if it makes sense semantically that EntityB is the principal part in the relationship. Otherwise I would recommend solution 4.

More information about the way one-to-one relationships are mapped by EF Core to the physical tables can be found here. A FK column is added only in the dependent table. No column is added to the principal table. The same that is done for a one-to-many relationship, except that, to make sure that the relationship is one-to-one and not one-to-many, a unique constraint is created also on that FK column.

EF does this by convention for one-to-one relationship between two entities that have a navigation property in both sides of the relationship (which is the case in Solution 2 scenary, but can't be used in Solution 1).

This page also contains info that can be relevant for your issue.

Sorry, this answer got longer that I intended. I got a bit carried away.



来源:https://stackoverflow.com/questions/49219379/ef-core-two-one-to-one-on-one-principal-key

易学教程内所有资源均来自网络或用户发布的内容,如有违反法律规定的内容欢迎反馈
该文章没有解决你所遇到的问题?点击提问,说说你的问题,让更多的人一起探讨吧!