无法确定 X 关系的主端.多个添加的实体可能具有相同的主键
本文关键字:添加 关系 无法确定 实体 | 更新日期: 2024-10-20 06:11:40
我知道还有其他人问过同样的问题,答案是处理引用而不是ID。
就我而言,我有一个奇怪的实体框架行为:它在一种情况下(父子(有效,但在另一种情况下(子孙子(无效。
这是我的模型:
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; }
}
这是我的映射:
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");
}
}
这是我的代码:
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();
此代码失败,并显示消息
'无法确定Child_GrandChildren的主要端 关系。多个添加的实体可能具有相同的主键'
但是如果我在添加孩子后保存,而我确实需要在添加父级后调用SaveChanges()
,则可以工作。
编辑:如果我删除属性List<GrandChild> GrandChildren
它可以工作,但我真的需要它。
这是一个错误吗?
更改关系配置,ChildMap
以下内容:
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);
您应该将 Child 对象添加到 Parent 对象,而不是直接将它们添加到上下文中。应将 GrandChild 对象添加到相应的 Child 对象。
在这种情况下,只应将 Parent 对象添加到上下文中,这样,将以正确的顺序在数据库中创建实体,并正确解析 FK。
您的代码应如下所示:
Parent parent = new Parent { Name = "Parent" };
Child child_1 = new Child { Name = "Child 1" };
parent.Children.Add(child_1);
Child child_2 = new Child { Name = "Child 2" };
parent.Children.Add(child_2);
GrandChild grandChild_1 = new GrandChild { Name = "GrandChild 1" };
GrandChild grandChild_2 = new GrandChild { Name = "GrandChild 2" };
child_2.GrandChildren.Add(grandChild_1);
child_2.GrandChildren.Add(grandChild_2);
context.Parents.Add(parent);
context.SaveChanges();
如果不需要引用添加的对象,则可以改用此 fluent 样式初始化代码:
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" }
}
}
}
};
context.Parents.Add(parent);
context.SaveChanges();
直接将子对象添加到上下文的主要问题是,当您像这样声明它们时,尤其是在流畅逻辑的情况下,操作顺序不能混淆,因此不容易推断将对象添加到数据库应遵循的顺序。
在最后一种情况下,您可能会遇到此异常,即当您具有递归或分层关系时。
当需要使用深层链接保存递归或分层数据时,应分多个步骤保存数据。不要害怕多次调用
SaveChanges()
,开销很小,对于较大的数据集,它实际上会提高性能以频繁保存,而不是在流程结束时尝试保存为单个操作。
如果您担心 ACID 主体或处理失败,这就是您避免调用
SaveChanges()
的原因,那么您应该将逻辑包装在事务中:using (var trans = context.Database.BeginTransaction()) { ... context.SaveChanges(); ... context.SaveChanges(); ... trans.Commit(); }
如果您使用
IDisposable
使用模式,甚至不需要捕获和处理异常。
- 注意:与纯 SQL 不同,EF 不支持嵌套事务。您可以
在原始帖子的上下文中,如果父母有最喜欢的Child
和/或最喜欢的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; }
}
在这种情况下,确保正确定义关系非常重要,并且您需要分 2 个步骤保存数据。
// 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);
保存数据需要分两个步骤完成。这个模型对此非常有效。一开始,Parent
没有孩子,后来又加了一Child
,这时,它可能不是最受欢迎的......稍后又添加了另一个Child
。尽管如此,Parent
还没有决定谁是最受欢迎的。后来,一个最喜欢的孩子被选中了。
让我们忽略一个现实世界的事实,即生孩子是定义
Parent
与Person
的原因......
数据逻辑需要尊重相同的思维过程。如果我们尝试将子对象定义为新父级的父级和子级,我们会遇到一些难题:对于要保存在数据库中的父对象,我们需要子记录的 ID,但要将子对象保存到数据库中,我们需要父记录的 ID...也许我们应该称这些为Chicken
和Egg
......
解决方案是先保存主要关系,然后返回并保存任何递归关系链接:
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();
}
如果您的孩子开始用他们的兄弟姐妹决定使用的名字来命名他们的孩子,那么这种选择逻辑将不起作用......但你明白了,我们应该善待我们的父母,因为对孩子使用独特的名字:)
或者回到OP的原始脚本。只需在分配之间调用SaveChanges()
,就可以避免所有这些,在这里使用事务范围可以解决 ACID 主体,如果引发异常或其中一个调用SaveChanges()
失败,可能会违反这些原则。
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();
}