简体   繁体   中英

Unable to determine the principal end of the X relationship. Multiple added entities may have the same primary key

I know there are others who have asked about the same problem, and the answer was to deal with references and not with IDs.

In my case I have a weird behaviour of entity framework : it works in one case (parent-child) but not in the other (child-grandchild).

Here are my models:

public class Parent
{

    public int ID { get; set; }
    public string Name { get; set; }

    public List<Child> Children { get; set; } = new List<Child>();
}


public class Child
{

    public int ID { get; set; }
    public int ParentID { get; set; }       
    public string Name { get; set; }
    public List<GrandChild> GrandChildren { get; set; } = new List<GrandChild>();  
    public Parent Parent { get; set; }

}

public class GrandChild
{

    public int ID { get; set; }
    public int ChildID { get; set; }
    public String Name { get; set; }      
    public Child Child { get; set; }     

}

And here's my mapping :

public class ParentConfig : EntityTypeConfiguration<Parent>
{
    public ParentConfig()
    {

        HasKey(e => e.ID);
        Property(e => e.ID).HasColumnName("ID");
        Property(e => e.Name).HasColumnName("Name");
        HasMany(e => e.Children).WithRequired(c => c.Parent).HasForeignKey(c => c.ParentID);

        ToTable("Parent");
    }
}

public class ChildMap : EntityTypeConfiguration<Child>
{
    public ChildMap()
    {

        HasKey(e => e.ID);

        Property(e => e.ID).HasColumnName("ID");
        Property(e => e.Name).HasColumnName("Name");
        Property(e => e.ParentID).HasColumnName("ParentID");

        HasMany(c => c.GrandChildren).WithRequired().HasForeignKey(c => c.ChildID);
        HasRequired(e => e.Parent).WithMany().HasForeignKey(e => e.ParentID);

        ToTable("Child");

    }
}


public class GrandChildMap : EntityTypeConfiguration<GrandChild>
{
    public GrandChildMap()
    {
        HasKey(e => e.ID);
        Property(e => e.ID).HasColumnName("ID");
        Property(e => e.ChildID).HasColumnName("ChildID");
        Property(e => e.Name).HasColumnName("Name");
        HasRequired(e => e.Child).WithMany().HasForeignKey(e => e.ChildID);
        ToTable("GrandChild");
    }
}

And here's my code:

        Parent parent = new Parent { Name = "Parent", };
        Child child_1 = new Child { Name = "Child 1", Parent = parent };
        Child child_2 = new Child { Name = "Child 2", Parent = parent };
        GrandChild grandChild_1 = new GrandChild { Name = "GrandChild 1", Child = child_2 };
        GrandChild grandChild_2 = new GrandChild { Name = "GrandChild 2", Child = child_2 };

            context.Parents.Add(parent);

            //no need to call SaveChanges

            context.Children.Add(child_1);
            context.Children.Add(child_2);

            //SaveChanges() is needed here

            context.GrandChildren.Add(grandChild_1);
            context.GrandChildren.Add(grandChild_2);

            context.SaveChanges();

This code fails with the message

'Unable to determine the principal end of the Child_GrandChildren relationship. Multiple added entities may have the same primary key'

But works if I save after adding the child, while I do need to call SaveChanges() after adding the parent.

EDIT : if I remove the property List<GrandChild> GrandChildren it works, but I really need it.

Is this a bug?

You need to change your relationship configuration in ChildMap to this:

  HasMany(c => c.GrandChildren).WithRequired(gc=>gc.Child).HasForeignKey(c => c.ChildID);
  // the second one is not necessary, you already configure that relationship in ParentConfig
  //HasRequired(e => e.Parent).WithMany().HasForeignKey(e => e.ParentID);

you need to be doing something like this:

parent.Children.Add(child1);
parent.Children.Add(child2);
child1.GrandChildren.Add(grandChild1);
....
context.SaveChanges();

There is a final scenario where you can experience this exception, that is when you have recursive or hierarchical relationships.

When Recursive or Hierachical data needs to be saved with deep links, you should save the data in multiple steps. Do not be afraid of calling SaveChanges() multiple times , the overheads are minimal, and for larger sets of data it will actually improve performance to save frequently rather than trying to save as a single action at the end of a process.

If you are concerned about ACID principals or handling failure and that is why you have avoided calling SaveChanges() then you should wrap your logic in a transaction :

 using (var trans = context.Database.BeginTransaction()) { ... context.SaveChanges(); ... context.SaveChanges(); ... trans.Commit(); }

Its not even necessary to catch and handle exceptions if you use the IDisposable using pattern.

  • NOTE: Unlike pure SQL, EF does not support nested transactions. You can

In the context of the original post, this might come up if the Parent has a favourite Child and/or a favourite GrandChild :

public class Parent
{
    public int ID { get; set; }
    public string Name { get; set; }

    public List<Child> Children { get; set; } = new List<Child>();
    public int? Favourite_ChildID { get; set; }
    public Child FavouriteChild { get;set; }
    public int? Favourite_GrandChildID { get; set; }
    public GrandChild FavouriteGrandChild { get;set; }
}

In this scenario it will be important to make sure that the relationships are correctly defined AND you will need to save the data in 2 steps.

// Parent Config
HasMany(p => p.Children)
    .WithRequired(c => c.Parent)
    .HasForeignKey(c => c.ParentID);
HasOptional(p => p.FavouriteChild)
    .WithMany()
    .HasForeignKey(p => p.Favourite_ChildID);
HasOptional(p => p.FavouriteGrandChild)
    .WithMany()
    .HasForeignKey(p => p.Favourite_GrandChildID);                         

// Child Config
HasMany(c => c.GrandChildren)
    .WithRequired(gc => gc.Child)
    .HasForeignKey(gc => gc.ChildID);

Saving the data needs to be done in two passes or steps. This model works really well for this. At the start, the Parent has no children, later a Child is added, at this point, it may not be the favourite... Later another Child is added. Still, the Parent has not yet decided who the favourite is. Later a Favourite child has been selected .

Lets overlook the real-world fact that having a child is what defines a Parent vs a Person ...

That same thought process needs to be respected by your data logic. If we try to define the child as both the faviourite and the child of the new parent we have a bit of a conundrum: For the parent object to be saved in the database we need the Id of the Child record, but for the Child to be saved into the database we need the Id of the Parent record... maybe we should have called these Chicken and Egg ...

The solution is to save the Primary relationships first, then come back and save any recursive relationship links:

Parent parent = new Parent 
{ 
    Name = "Parent" 
    Children = new List<Child> 
    {
        new Child { Name = "Child 1" },
        new Child 
        {
            Name = "Child 2",
            GrandChildren = new List<GrandChild>
            {
                new GrandChild { Name = "GrandChild 1" },
                new GrandChild { Name = "GrandChild 2" }
            }
        }     
    }
};

// using transaction scope here to demonstrate how to manage multiple SaveChanges with a rollback
using (var trans = context.Database.BeginTransaction())
{
    context.Parents.Add(parent);
    context.SaveChanges();

    parent.FavouriteChild = parent.Children.Single(child => child.Name == "Child 1");
    parent.FavouriteGrandChild = parent.Children.SelectMany(child => child.GrandChildren).Single(gc => gc.Name == "GrandChild 2");
    context.SaveChanges();

    trans.Commit();
}

This selection logic won't work if your kids start naming their kids with names that their siblings have decided to use... but you get the point, we should be kind to our Parents as use unique names for out children :)

Or going back to OPs original script. Simply calling SaveChanges() in between the assignments, then all of this would have been avoided, using a transaction scope here addresses ACID principals that could be violoated if and exceptions are raised or one of the calls to SaveChanges() fails.

using (var trans = context.Database.BeginTransaction())
{
    Parent parent = new Parent { Name = "Parent", };
    context.Parents.Add(parent);
    context.SaveChanges();

    Child child_1 = new Child { Name = "Child 1", Parent = parent };
    Child child_2 = new Child { Name = "Child 2", Parent = parent };
    context.Children.Add(child_1);
    context.Children.Add(child_2);
    context.SaveChanges();

    parent.FavouriteChild = child_1;
    // we can save this next time, no Ids need to be forced.

    GrandChild grandChild_1 = new GrandChild { Name = "GrandChild 1", Child = child_2 };
    GrandChild grandChild_2 = new GrandChild { Name = "GrandChild 2", Child = child_2 };
    context.GrandChildren.Add(grandChild_1);
    context.GrandChildren.Add(grandChild_2);
    context.SaveChanges();

    // Now we can assign the faviourite GrandChild
    parent.FavouriteGrandChild = grandChild_2;

    context.SaveChanges();

    // Actually commit the changes to the database
    trans.Commit();
}

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM