Does Entity Framework support circular references?

前端 未结 4 2065
感动是毒
感动是毒 2021-01-04 06:42

I have two entities in parent/child relationship. In addition, parent contains a reference to a \"main\" child, so the simplified model looks like this:

clas         


        
4条回答
  •  悲&欢浪女
    2021-01-04 07:08

    This is an old question but still relevant with Entity Framework 6.2.0. My solution is three-fold:

    1. Do NOT set the MainChildId column as HasDatabaseGeneratedOption(Computed) (this blocks you from updating it later)
    2. Use a Trigger to update the Parent when I'm inserting both records simultaneously (this isn't a problem if the parent already exists and I'm just adding a new child, so be sure the Trigger accounts for this somehow - was easy in my case)
    3. After calling ctx.SaveChanges(), also be sure to call ctx.Entry(myParentEntity).Reload() to get any updates to the MainChildId column from the Trigger (EF won't automatically pick these up).

    In my code below, Thing is the parent and ThingInstance is the child and has these requirements:

    • Whenever a Thing (parent) is inserted, a ThingInstance (child) should also be inserted and set as the Thing's CurrentInstance (main child).
    • Other ThingInstances (children) may be added to a Thing (parent) with or without becoming the CurrentInstance (main child)

    This resulted in the following design: * EF Consumer must insert both records but leave CurrentInstanceId as null but be sure to set ThingInstance.Thing to the parent. * Trigger will detect if a ThingInstance.Thing.CurrentInstanceId is null. If so, then it will update it to the ThingInstance.Id. * EF Consumer must reload/refetch the data to view any updates by the trigger. * Two round-trips are still necessary but only one atomic call to ctx.SaveChanges is necessary and I don't have to deal with manual rollbacks. * I do have an extra trigger to manage, and there might be a more efficient way to do it than what I've done here with a cursor, but I'll never be doing this in a volume where performance will matter.

    Database:

    (Sorry, not tested this script - just generated it from my DB and put it here due to being in a hurry. You should definitely be able to get the important bits out of here.)

    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# Inserts:

    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# New Child:

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

提交回复
热议问题