我有父/子关系中的两个实体。此外,parent包含对“main”子项的引用,因此简化模型如下所示:
class Parent
{
int ParentId;
int? MainChildId;
}
class Child
{
int ChildId;
int ParentId;
}
我现在遇到的问题是,EF似乎无法在单个操作中处理父和子的创建。我收到错误“System.Data.UpdateException:无法确定依赖操作的有效排序。由于外键约束,模型要求或存储生成的值,可能存在依赖关系。”
MainChildId可以为空,因此应该可以生成父项,子项,然后使用新生成的ChildId更新父项。这是EF不支持的东西吗?
答案 0 :(得分:5)
不,它得到了支持。使用GUID键或可指定序列进行尝试。错误意味着它的确如此:EF无法一步到位地弄清楚如何做到这一点。但是,您可以分两步完成(两次调用SaveChanges()
)。
答案 1 :(得分:5)
我有这个问题。明显的“循环引用”就是很好的数据库设计。像“IsMainChild”这样的子表上有一个标志是糟糕的设计,属性“MainChild”是父项的属性而不是子项,因此父项中的FK是合适的。
EF4.1需要找到一种本地处理这种类型关系的方法,而不是强迫我们重新设计我们的数据库以适应框架中的缺陷。
无论如何,我的解决方法是执行几个步骤(就像你在编写存储过程时一样),唯一的问题就是围绕上下文更改跟踪。
Using context As New <<My DB Context>>
' assuming the parent and child are already attached to the context but not added to the database yet
' get a reference to the MainChild but remove the FK to the parent
Dim child As Child = parent.MainChild
child.ParentID = Nothing
' key bit detach the child from the tracking context so we are free to update the parent
' we have to drop down to the ObjectContext API for that
CType(context, IObjectContextAdapter).ObjectContext.Detach(child)
' clear the reference on the parent to the child
parent.MainChildID = Nothing
' save the parent
context.Parents.Add(parent)
context.SaveChanges()
' assign the newly added parent id to the child
child.ParentID = parent.ParentID
' save the new child
context.Children.Add(child)
context.SaveChanges()
' wire up the Fk on the parent and save again
parent.MainChildID = child.ChildID
context.SaveChanges()
' we're done wasn't that easier with EF?
End Using
答案 2 :(得分:1)
EF和LINQ to SQL都存在无法保存循环引用的问题,即使它们在幕后为您封装2个或更多SQL调用而不是抛出异常。
我在LINQ to SQL中为此编写了一个修复程序,但还没有在EF中这样做,因为我暂时在数据库设计中避免使用循环引用。
你可以做的是创建一个辅助方法来保留循环引用,在调用SaveChanges()之前运行它,运行另一个将循环引用放回原位的方法,然后再次调用SaveChanges()。您可以将所有这些封装在一个方法中,可能是SaveChangesWithCircularReferences()
。
要放回循环引用,您需要跟踪删除的内容并返回该日志。
public class RemovedReference() . . .
public List<RemovedReference> SetAsideReferences()
{
. . .
}
因此,SetAsideReferences中的代码基本上是搜索循环引用,在每种情况下留出一半,并将它们记录在列表中。
在我的例子中,我创建了一个存储对象,属性名称和删除的值(另一个对象)的类,并将它们保存在列表中,如下所示:
public class RemovedReference
{
public object Object;
public string PropertyName;
public object Value;
}
可能有一个更聪明的结构来实现这一目标;例如,您可以使用PropertyInfo对象而不是字符串,并且可以缓存该类型以避免第二轮反射。
答案 3 :(得分:1)
这是一个老问题,但仍与Entity Framework 6.2.0相关。我的解决方案有三个方面:
MainChildId
列设为HasDatabaseGeneratedOption(Computed)
(这会阻止您稍后更新)ctx.SaveChanges()
后,请务必致电ctx.Entry(myParentEntity).Reload()
,以便从触发器中获取MainChildId
列的任何更新(EF不会自动选择这些内容)。< / LI>
醇>
在我的代码中,Thing
是父级,ThingInstance
是孩子,并且有以下要求:
Thing
(父)时,也应插入ThingInstance
(子)并将其设置为Thing
&#39; s CurrentInstance
(主要)子)。ThingInstances
(子女)可以添加到Thing
(父母),无论是否成为CurrentInstance
(主要子女)这导致以下设计:
* EF Consumer必须插入两个记录,但将CurrentInstanceId
保留为null,但请务必将ThingInstance.Thing
设置为父级。
*触发器将检测ThingInstance.Thing.CurrentInstanceId
是否为空。如果是,那么它会将其更新为ThingInstance.Id
。
* EF Consumer必须重新加载/重新获取数据才能通过触发器查看任何更新。
*仍然需要两次往返,但只需要对ctx.SaveChanges
进行一次原子调用,而且我不必处理手动回滚。
*我确实有一个额外的触发器来管理,并且可能有一种更有效的方法来完成它,而不是我在这里用光标做的事情,但是我永远不会在性能将会这样做的卷中这样做物质
(对不起,没有测试过这个脚本 - 只是从我的数据库生成它并把它放在这里因为匆忙。你绝对应该能够从这里得到重要的部分。)
CREATE TABLE [dbo].[Thing](
[Id] [bigint] IDENTITY(1,1) NOT NULL,
[Something] [nvarchar](255) NOT NULL,
[CurrentInstanceId] [bigint] NULL,
CONSTRAINT [PK_Thing] PRIMARY KEY CLUSTERED
(
[Id] ASC
)WITH (STATISTICS_NORECOMPUTE = OFF, IGNORE_DUP_KEY = OFF) ON [PRIMARY]
) ON [PRIMARY]
GO
CREATE TABLE [dbo].[ThingInstance](
[Id] [bigint] IDENTITY(1,1) NOT NULL,
[ThingId] [bigint] NOT NULL,
[SomethingElse] [nvarchar](255) NOT NULL,
CONSTRAINT [PK_ThingInstance] PRIMARY KEY CLUSTERED
(
[Id] ASC
)WITH (STATISTICS_NORECOMPUTE = OFF, IGNORE_DUP_KEY = OFF) ON [PRIMARY]
) ON [PRIMARY]
GO
ALTER TABLE [dbo].[Thing] WITH CHECK ADD CONSTRAINT [FK_Thing_ThingInstance] FOREIGN KEY([CurrentInstanceId])
REFERENCES [dbo].[ThingInstance] ([Id])
GO
ALTER TABLE [dbo].[Thing] CHECK CONSTRAINT [FK_Thing_ThingInstance]
GO
ALTER TABLE [dbo].[ThingInstance] WITH CHECK ADD CONSTRAINT [FK_ThingInstance_Thing] FOREIGN KEY([ThingId])
REFERENCES [dbo].[Thing] ([Id])
ON DELETE CASCADE
GO
ALTER TABLE [dbo].[ThingInstance] CHECK CONSTRAINT [FK_ThingInstance_Thing]
GO
CREATE TRIGGER [dbo].[TR_ThingInstance_Insert]
ON [dbo].[ThingInstance]
AFTER INSERT
AS
BEGIN
SET NOCOUNT ON;
DECLARE @thingId bigint;
DECLARE @instanceId bigint;
declare cur CURSOR LOCAL for
select Id, ThingId from INSERTED
open cur
fetch next from cur into @instanceId, @thingId
while @@FETCH_STATUS = 0 BEGIN
DECLARE @CurrentInstanceId bigint = NULL;
SELECT @CurrentInstanceId=CurrentInstanceId FROM Thing WHERE Id=@thingId
IF @CurrentInstanceId IS NULL
BEGIN
UPDATE Thing SET CurrentInstanceId=@instanceId WHERE Id=@thingId
END
fetch next from cur into @instanceId, @thingId
END
close cur
deallocate cur
END
GO
ALTER TABLE [dbo].[ThingInstance] ENABLE TRIGGER [TR_ThingInstance_Insert]
GO
public Thing Inserts(long currentId, string something)
{
using (var ctx = new MyContext())
{
Thing dbThing;
ThingInstance instance;
if (currentId > 0)
{
dbThing = ctx.Things
.Include(t => t.CurrentInstance)
.Single(t => t.Id == currentId);
instance = dbThing.CurrentInstance;
}
else
{
dbThing = new Thing();
instance = new ThingInstance
{
Thing = dbThing,
SomethingElse = "asdf"
};
ctx.ThingInstances.Add(instance);
}
dbThing.Something = something;
ctx.SaveChanges();
ctx.Entry(dbThing).Reload();
return dbThing;
}
}
public Thing AddInstance(long thingId)
{
using (var ctx = new MyContext())
{
var dbThing = ctx.Things
.Include(t => t.CurrentInstance)
.Single(t => t.Id == thingId);
dbThing.CurrentInstance = new ThingInstance { SomethingElse = "qwerty", ThingId = dbThing.Id };
ctx.SaveChanges(); // Reload not necessary here
return dbThing;
}
}