实体框架是否支持循环引用?

时间:2010-12-23 09:49:12

标签: entity-framework

我有父/子关系中的两个实体。此外,parent包含对“main”子项的引用,因此简化模型如下所示:

class Parent
{
   int ParentId;
   int? MainChildId;
}

class Child
{
   int ChildId;
   int ParentId;
}

我现在遇到的问题是,EF似乎无法在单个操作中处理父和子的创建。我收到错误“System.Data.UpdateException:无法确定依赖操作的有效排序。由于外键约束,模型要求或存储生成的值,可能存在依赖关系。”

MainChildId可以为空,因此应该可以生成父项,子项,然后使用新生成的ChildId更新父项。这是EF不支持的东西吗?

4 个答案:

答案 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相关。我的解决方案有三个方面:

  1. 请勿MainChildId列设为HasDatabaseGeneratedOption(Computed)(这会阻止您稍后更新)
  2. 当我同时插入两个记录时,使用触发器来更新父级(如果父级已经存在并且我只是添加一个新的孩子,这不是问题,所以请确保触发器以某种方式解释这个问题 - 在我的案例中很容易)
  3. 致电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
    

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