Entity Framework支持循环引用吗?

13

我有两个存在父子关系的实体。此外,父实体包含一个指向“主”子实体的引用,因此简化模型如下:

class Parent
{
   int ParentId;
   int? MainChildId;
}

class Child
{
   int ChildId;
   int ParentId;
}
我现在遇到的问题是EF似乎无法处理在单个操作中创建父对象和子对象。我收到错误提示“System.Data.UpdateException: Unable to determine a valid ordering for dependent operations. Dependencies may exist due to foreign key constraints, model requirements, or store-generated values.”
MainChildId是可空的,所以应该可以先生成一个父对象和一个子对象,然后再使用新生成的ChildId更新父对象。EF是否不支持这种操作?
4个回答

5
我遇到过这个问题。看似“循环引用”实际上是良好的数据库设计。在子表中添加像“IsMainChild”这样的标志是不好的设计,属性“MainChild”是父级而非子级的属性,因此在父表中使用外键是适当的。
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  

我完全同意你的观点。这是Stack上唯一一个我能找到有人遇到和我一样问题,但知道他们拥有良好的数据库设计却无法被EF满足的答案。真的很烦人,它不能在内部处理这个问题,我们只能通过多次调用SaveChanges()来绕过它。 - theyetiman

5
不,它是被支持的。尝试使用GUID键或可分配序列进行操作。该错误的意思与其所述完全相同:EF无法在一步中完成此操作。但你可以通过两个步骤来完成(两次调用SaveChanges())。

3
如果需要两个步骤,那么我会说这是不受支持的,因为在原始描述中,我提到想要在一个操作中处理它,即不需要多次调用SaveChanges。我收到了微软的回答,他们说当前版本的EF不能处理此操作:http://social.msdn.microsoft.com/Forums/en-US/adodotnetentityframework/thread/d8691dc2-bb13-4e5d-959b-2ae40a9caec5 - Vagif Abilov
1
在 SQL 中以原子方式完成这个操作没有问题:只需在创建父记录并检索其标识后添加更新语句即可。我已经在 Microsoft Connect 上创建了一个请求,希望添加此功能。如果使用自动递增整数键,则无法使用可分配的键。 - Vagif Abilov
“...并检索其标识。”没错!你需要不止一个语句。你是在谈论一个事务吗?你可以使用TransactionScope在EF中完成这个操作。 - Craig Stuntz
我相信你的意思是它不被支持。我编辑了你的答案,但它被拒绝了。 - Marc.2377
@marc.2337 不,我指的是我所写的。 - Craig Stuntz
显示剩余3条评论

1
这是一个旧问题,但在Entity Framework 6.2.0中仍然相关。 我的解决方案有三个步骤:
  1. 不要MainChildId列设置为HasDatabaseGeneratedOption(Computed)(否则您将无法随后更新它)
  2. 使用触发器在同时插入两条记录时更新父记录(如果父记录已存在且您只是添加新的子记录,则这不是问题,请确保触发器以某种方式考虑到这一点 - 在我的情况下很容易)
  3. 在调用ctx.SaveChanges()之后,还要确保调用ctx.Entry(myParentEntity).Reload()从触发器获取MainChildId列的任何更新(EF不会自动获取这些更新)。
在我的代码中,Thing是父记录,ThingInstance是子记录,并具有以下要求:
  • 每当插入一个Thing(父级)时,也应该插入一个ThingInstance(子级),并将其设置为ThingCurrentInstance(主要子级)。
  • 其他ThingInstances(子级)可以添加到Thing(父级)中,可以成为CurrentInstance(主要子级),也可以不成为。

这导致了以下设计: * EF消费者必须插入两个记录,但将CurrentInstanceId保留为空,但一定要确保将ThingInstance.Thing设置为父级。 * 触发器将检测是否ThingInstance.Thing.CurrentInstanceId为空。如果是,则将其更新为ThingInstance.Id。 * EF消费者必须重新加载/获取数据以查看触发器的任何更新。 * 仍然需要两次往返,但只需要一次原子调用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

C# 插入:

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

C# 新建子项:

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

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 对象而不是字符串,并且您可以缓存类型以降低第二轮反射的成本。


看起来使用可空的外键,EF也可以一次性为您完成此操作:https://dev59.com/1G855IYBdhLWcg3wg0jC - Chris Moschini
不完全正确。您链接的答案是指与同一表的关系。 - Marc.2377

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接