c# - Entity Framework 以错误的顺序插入子对象

标签 c# entity-framework tsql relational-database parent-child

问题

为什么 EF 首先在它所依赖的对象 (TimesheetActivity) 之前插入一个具有依赖关系的子对象 (PersonnelWorkRecord)。另外,我有哪些纠正此问题的选择?

ERD(简体)

这是由另一个不受我直接控制的系统预定义的。 ERD2

EF设置和保存代码

我不确定我是否理解 Entity Framework 为什么/如何按它的顺序插入我拥有的对象,但是这是我用来插入一个父项和多个子项的代码。

using (var db = new DataContext(user))
{
     timesheet.State = State.Added;
     timesheet.SequenceNumber = newSequenceNumber;
     this.PrepareAuditFields(timesheet);

     //To stop EF from trying to add all child objects remove them from the timehseets object.
     timesheet = RemoveChildObjects(timesheet, db);

     //Add the Timesheet object to the database context, and save.
     db.Timesheets.Add(timesheet);
     result = db.SaveChanges() > 0;
}

EF 插入的 SQL 跟踪

当我运行代码时,我在 PersonnelWorkRecord (TimesheetActivityID) 上发现了 SQL 外键冲突,因为我还没有添加事件(请参阅跟踪)。

exec sp_executesql N'insert [dbo].[Timesheets]([ProjectID], [TimesheetStatusID], ...
exec sp_executesql N'insert [dbo].[PersonnelWorkdays]([TimesheetID], [PersonnelID], ...
exec sp_executesql N'insert [dbo].[PersonnelWorkRecords]([PersonnelWorkdayID],[TimesheetActivityID], ...

数据上下文摘要

modelBuilder.Entity<PersonnelWorkday>().HasRequired(pwd => pwd.Personnel).WithMany(p => p.PersonnelWorkdays).HasForeignKey(pwd => pwd.PersonnelID).WillCascadeOnDelete(false);
modelBuilder.Entity<PersonnelWorkday>().HasRequired(pwd => pwd.Timesheet).WithMany(t => t.PersonnelWorkdays).HasForeignKey(pwd => pwd.TimesheetID).WillCascadeOnDelete(false);
modelBuilder.Entity<PersonnelWorkRecord>().HasRequired(pwr => pwr.PersonnelWorkday).WithMany(pwd => pwd.PersonnelWorkRecords).HasForeignKey(pwr => pwr.PersonnelWorkdayID).WillCascadeOnDelete(false);
modelBuilder.Entity<PersonnelWorkRecord>().HasRequired(pwr => pwr.TimesheetActivity).WithMany(ta => ta.PersonnelWorkRecords).HasForeignKey(pwr => pwr.TimesheetActivityID).WillCascadeOnDelete(false);
modelBuilder.Entity<TimesheetActivity>().HasRequired(ta => ta.ProjectActivity).WithMany(a => a.TimesheetActivities).HasForeignKey(ta => ta.ProjectActivityCodeID).WillCascadeOnDelete(false);
modelBuilder.Entity<TimesheetActivity>().HasOptional(ta => ta.Facility).WithMany(f => f.TimesheetActivities).HasForeignKey(tf => tf.FacilityID).WillCascadeOnDelete(false);
modelBuilder.Entity<TimesheetActivity>().HasRequired(ta => ta.Timesheet).WithMany(t => t.TimesheetActivities).HasForeignKey(ta => ta.TimesheetID).WillCascadeOnDelete(false);

删除子对象

这是子对象方法的代码。我添加了此方法以从时间表的子对象相关对象中删除不是外键的对象。例如,我有一个 Crew 对象,但我也有一个 CrewID 外键,所以我设置了 Crew = null,这样 EF 就不会尝试插入它,因为它已经存在。

private Timesheet RemoveChildObjects(Timesheet timesheet, DataContext db)
{
        timesheet.Crew = null;
        timesheet.Foreman = null;
        timesheet.Location = null;
        timesheet.Project = null;
        timesheet.SigningProjectManager = null;
        timesheet.TimesheetStatus = null;
        timesheet.Creator = null;
        timesheet.Modifier = null;

        if (timesheet.TimesheetActivities != null)
        {
            foreach (TimesheetActivity tsa in timesheet.TimesheetActivities)
            {
                tsa.Creator = null;
                if (tsa.EquipmentWorkRecords != null)
                {
                    tsa.EquipmentWorkRecords = RemoveChildObjects(tsa.EquipmentWorkRecords, db);
                }
                tsa.Facility = null;
                tsa.Modifier = null;
                if (tsa.PersonnelWorkRecords != null)
                {
                    tsa.PersonnelWorkRecords = RemoveChildObjects(tsa.PersonnelWorkRecords, db);
                }
                tsa.ProjectActivity = null;
                tsa.Structures = null;
                tsa.Timesheet = null;
            }
        }

        if (timesheet.TimesheetEquipment != null)
        {
            foreach (TimesheetEquipment te in timesheet.TimesheetEquipment)
            {
                te.Equipment = null;
                te.Timesheet = null;
            }
        }

        if (timesheet.EquipmentWorkdays != null)
        {
            timesheet.EquipmentWorkdays = RemoveChildObjects(timesheet.EquipmentWorkdays, true, db);
        }

        if (timesheet.TimesheetPersonnel != null)
        {
            foreach (TimesheetPersonnel tp in timesheet.TimesheetPersonnel)
            {
                tp.Personnel = null;
                tp.PersonnelWorkday = null;
                if (tp.PersonnelWorkday != null)
                {
                    tp.PersonnelWorkday = RemoveChildObjects(tp.PersonnelWorkday, db);
                }
                tp.Timesheet = null;
            }
        }

        if (timesheet.PersonnelWorkdays != null)
        {
            timesheet.PersonnelWorkdays = RemoveChildObjects(timesheet.PersonnelWorkdays, true, db);
        }

        return timesheet;
    }

EF 保存前值的调试

根据我的理解,当调用 dbContext.Save() 时,将添加/修改/删除 dbContex.ObjectNameHere.Local。 (也取决于实体状态的设置。)这是在我调用 save() 并获得 SQL FK 异常之前 EF 试图保存的内容。 Step1 Step2 然后我得到 FK 异常。

The INSERT statement conflicted with the FOREIGN KEY constraint "FK_PersonnelWorkRecords_TimesheetActivities". The conflict occurred in database "VPMTEST_GC", table "dbo.TimesheetActivities", column 'TimesheetActivityID'. The statement has been terminated.

注意事项

如果我可以发布任何内容来帮助描述我的问题,请告诉我。我在 google/SO 上四处寻找答案,但到目前为止还没有可靠的答案,看起来 EF 无法确定插入对象的顺序,除非域模型的设置不同?我无法更改大多数对象的结构,因为它们被另一个系统使用。我可以尝试更改我的 EF 调用,我宁愿不使用原始 SQL,因为对象比我在此处发布的简化版本要广泛得多。

类似问题: Self referencing entity and insert order

最佳答案

在您的 RemoveChildObjects 方法中,我看到了一行...

tsa.Timesheet = null;

显然,您正在将 Timesheet.TimesheetActivities 的反向导航属性设置为 null。您是否对 PersonnelWorkRecord.TimesheetActivityPersonnelWorkRecord.PersonnelWorkday 执行相同的操作,即您是否也在嵌套的 中将这些属性设置为 null >RemoveChildObjects 方法?

这可能是个问题,因为从 TimesheetPersonnelWorkRecord 有两条不同的路径,即:

Timesheet -> TimesheetActivities -> PersonnelWorkRecords
Timesheet -> PersonnelWorkdays -> PersonnelWorkRecords

当您调用 db.Timesheets.Add(timesheet) 时,我相信 EF 将逐个遍历对象图中的每个分支,并确定路径上哪些相关对象(“节点”)是相关的并且在确定插入顺序的关系中是主要的。 timesheet 本身是其所有关系的主体,因此很明显必须首先插入它。然后 EF 开始循环访问集合 Timesheet.TimesheetActivitiesTimesheet.PersonnelWorkdays 之一。谁先到并不重要。显然 EF 以 Timesheet.PersonnelWorkdays 开头。 (如果它以 Timesheet.TimesheetActivities 开头,则不会解决问题,您会得到相同的异常,但使用 PersonnelWorkRecord.PersonnelWorkday 而不是 PersonnelWorkRecord.TimesheetActivity 。)PersonnelWorkday 仅依赖于已插入的 Timesheet。因此,也可以插入 PersonnelWorkday

然后 EF 继续遍历 PersonnelWorkday.PersonnelWorkRecords。关于 PersonnelWorkRecordPersonnelWorkday 依赖项,同样没有问题,因为之前已经插入了 PersonnelWorkday。但是,当 EF 遇到 PersonnelWorkRecordTimesheetActivity 依赖项时,它将看到此 TimesheetActivitynull(因为您已经将其设置为 null)。现在假设依赖项由外键属性 TimesheetActivityID 单独描述,它必须引用现有记录。它插入 PersonnelWorkRecord,这违反了外键约束。

如果 PersonnelWorkRecord.TimesheetActivity 不是 null EF 将检测到此对象尚未插入,但它是 PersonnelWorkRecord 的主体.因此,它可以确定此 TimesheetActivity 必须插入 before PersonnelWorkRecord

如果您不将反向导航属性设置为null,我希望您的代码能正常工作——或者至少不是PersonnelWorkRecord 中的两个导航属性。 (将其他导航属性(如 tsa.Creatortsa.Facility 等设置为 null 应该不是问题,因为那些相关对象确实已经存在于数据库中,并且您已经为它们设置了正确的 FK 属性值。)

关于c# - Entity Framework 以错误的顺序插入子对象,我们在Stack Overflow上找到一个类似的问题: https://stackoverflow.com/questions/21869330/

相关文章:

c# - 给定数字之间的随机加倍

c# - 如何在局部 View MVC.Core 中使用 @section 脚本

c# - 如何使用 EntityFramework 6.2 将 DbFunctions/SqlFunctions 调用提取到可重复使用的扩展/方法中?

asp.net - EntityDataSource,其中关联计数 > 0

mysql - 如何将重复数据值分组到我的脚本中

sql - 如何在 SQL 中循环连接?

c# - WCF 和 nHibernate 的并发问题

C# - 在二进制文件中搜索模式

c# - 'Microsoft.EntityFrameworkCore.Infrastructure.IDbContextFactory`1[TContext ]' violates the constraint of type parameter ' TContext'

sql-server - SQL Server 中的分页