EF 实体在更新时未保存相同实体类型的子属性

Posted

技术标签:

【中文标题】EF 实体在更新时未保存相同实体类型的子属性【英文标题】:EF entity is not saving child property of same entity type on update 【发布时间】:2017-08-26 04:44:15 【问题描述】:

我正在使用 ASP.NET MVC 5 和 Entity Framework 6。我有一个页面允许用户输入进程信息。此信息的一个方面是从启动过程的下拉列表中进行选择。这个类大致是这样的:

**

public class SupportProcess
  
    [Key]
    public int ProcessId  get; set; 
    [DisplayName("Starting process?")]
    public virtual SupportProcess StartProcess  get; set; 
    public string Name  get; set; 
    [DisplayName("When is this run?")]
    public virtual ProcessSchedule ProcessSchedule  get; set; 
    [DisplayName("")]
    public string Description  get; set; 
    [DisplayName("Expected Result")]
    public string ExpectedResult  get; set; 
  

**

我正在使用一个视图模型,它具有 SupportProcess 的属性、选定的启动进程和一个进程列表来填充视图上的下拉列表。

  public class SupportProcessViewModel
  
    public SupportProcess SupportProcess  get; set; 
    public int SelectedStartProcess  get; set; 
    public List<SupportProcess> Processes  get; set; 

    public SupportProcessViewModel()
    
      this.SupportProcess = new SupportProcess();
    
  

我的编辑帖子操作如下所示:

   [HttpPost]
    [ValidateAntiForgeryToken]
    public ActionResult Edit(SupportProcessViewModel vm)
    
        if (ModelState.IsValid)
                      

      if (vm.SelectedStartProcess > 0)
      
        vm.SupportProcess.StartProcess = db.SupportProcesses.Find(vm.SelectedStartProcess);
      
        db.Entry(vm.SupportProcess).State = EntityState.Modified;
        db.SaveChanges();
        return RedirectToAction("Index");
    
    return View(vm);

问题在于,虽然 vm.SelectedStartProcess 不为 null 并且具有适当的值,但它永远不会保存到数据库中。数据库将此字段显示为 StartProcess_ProcessId。另外值得注意的是,一个进程可能有 0 或 1 个正在启动的进程。

我想知道 EF 已将数据库表中的此属性设为外键(本质上指向同一个表)这一事实是否会导致问题。我很乐意听取建议。

我还要补充一点,“创建帖子”操作按预期工作。这一定与向 EF 传达 StartProcess 属性也需要在实体上更新有关。虽然这是一个猜测...

    [HttpPost]
    [ValidateAntiForgeryToken]
    public ActionResult Create( SupportProcessViewModel vm)
    
        if (ModelState.IsValid)
        
            if(vm.SelectedStartProcess > 0) 
              vm.SupportProcess.StartProcess = db.SupportProcesses.Find(vm.SelectedStartProcess);
            
            db.SupportProcesses.Add(vm.SupportProcess);
            db.SaveChanges();
            return RedirectToAction("Index");
        

        return View(vm);
    

【问题讨论】:

【参考方案1】:

没有显式原始 FK 属性的引用导航属性的问题是影子 FK 状态由从中检索实体的DbContext 维护。在像您这样的断开连接的情况下,哪些信息会丢失。

当你调用时

db.Entry(entity).State = EntityState.Modified;

对于断开连接的实体,EF 会将实体附加到上下文并将所有 原始 属性标记为已修改。参考导航属性被视为未更改,因此在您调用 SaveChanges 时不会更新它们。

要让 EF 更新 FK,了解原始和新的参考属性值至关重要。在您的情况下,传递的 SupportProcess 实体的 StartProcess 属性的原始值。

由于在断开连接的情况下(尤其是像你这样的延迟加载属性)你不能依赖传递的对象属性值,我建议以下安全的操作顺序:

(1) 将导航属性设置为null 以避免将值附加到上下文。 (2) 将实体附加到上下文并将其标记为已修改。 (3) 显式加载导航属性。这将导致额外的数据库故障,但会确保更新正常工作。 (4) 设置导航属性的新值。当您调用SaveChanges 时,上下文更改跟踪器将能够确定是否需要更新 FK。

将其应用于您的案例:

[HttpPost]
[ValidateAntiForgeryToken]
public ActionResult Edit(SupportProcessViewModel vm)
        
    if (ModelState.IsValid)
    
        // (1)
        vm.SupportProcess.StartProcess = null;
        // (2)  
        db.Entry(vm.SupportProcess).State = EntityState.Modified;
        // (3)
        db.Entry(vm.SupportProcess).Reference(e => e.StartProcess).Load();
        // (4)
        vm.SupportProcess.StartProcess =
            vm.SelectedStartProcess > 0 ?
            db.SupportProcesses.Find(vm.SelectedStartProcess) :
            null;

        db.SaveChanges();
        return RedirectToAction("Index");
    
    return View(vm);

【讨论】:

该代码逐字运行,您的解释非常出色。这正是我申请赏金时所希望的那种答案!直到明天我才能给你赏金,但这是你的。感谢您提供出色、信息丰富且正确的回复!【参考方案2】:

我认为您的Edit ActionResult 应该是这样的

    [HttpPost]
    [ValidateAntiForgeryToken]
    public ActionResult Edit(SupportProcessViewModel vm)
    
        if (ModelState.IsValid)
                      

      if (vm.SelectedStartProcess > 0)
      
        vm.SupportProcess.StartProcess = db.SupportProcesses.Find(vm.SelectedStartProcess);
      
        db.SupportProcess.Attach(vm.SupportProcess);
        db.Entry(vm.SupportProcess).State = EntityState.Modified;
        db.SaveChanges();
        return RedirectToAction("Index");
    
    return View(vm);

【讨论】:

我认为您是对的,但是该语句在其余代码中的位置很重要。你能指出 OP 的代码应该是什么样子吗? 我会在那个星期一试试。还想知道我的编辑是否需要获取子 StartProcess 对象的 ProcessId 并将其作为隐藏字段包含在页面上,因此它最初可能是模型绑定在帖子和数据库上下文的一部分上。明天两个都试试。谢谢! 我试过这个。没有骰子。更新顺利进行,但 vm.SupportProcess.StartProcess.ProcessId 值绝不是更新的一部分。我实际上正在使用 Sql Profiler,所以我看到了针对数据库运行的确切 sql,并且我想要更新的字段除了在创建时从未包含在内。【参考方案3】:

确保您的模型配置正确:

public class SupportProcess

    [Key]
    public int ProcessId  get; set; 
    [DisplayName("Starting process?")]
    public int StartProcessId  get; set; 
    [ForeignKey("StartProcessId")]
    public virtual SupportProcess StartProcess  get; set; 
    public string Name  get; set; 
    [DisplayName("When is this run?")]
    public virtual ProcessSchedule ProcessSchedule  get; set; 
    [DisplayName("")]
    public string Description  get; set; 
    [DisplayName("Expected Result")]
    public string ExpectedResult  get; set; 

还有你的编辑方法:

[HttpPost]
[ValidateAntiForgeryToken]
public ActionResult Edit(SupportProcessViewModel vm)

    if (!ModelState.IsValid)
    
        return View(vm);   
               

    // Get the item
    var sp = db.SupportProcesses.FirstOrDefault(p => p.Id == vm.SupportProcess.ProcessId);

    // Set the new values
    if (vm.SelectedStartProcess > 0)
    
        sp.StartProcessId = vm.SelectedStartProcess;
    
    sp.Name = vm.SupportProcess.Name;
    sp.Description = vm.SupportProcess.Description;
    // all the rest values        

    // Save changes
    db.SupportProcesses.Update(sp);
    db.SaveChanges();

    return RedirectToAction("Index");
    

【讨论】:

以上是关于EF 实体在更新时未保存相同实体类型的子属性的主要内容,如果未能解决你的问题,请参考以下文章

EF6:如何在 Select 中包含子属性,以便创建单个实例。避免“相同的主键”错误

ef-codefirst方式配置实体类,生成数据库

实体有两个属性,它们都在一对多关系中引用相同的实体类型

一个存储实体属性是不是只能映射到EF中的一个类实体属性?

更新 EF 中的实体属性,其中属性是另一个实体

EF中关于实体类枚举问题