如何仅保存/更新父实体而不将其子实体保存在 asp.net mvc 的 EF6 中?

Posted

技术标签:

【中文标题】如何仅保存/更新父实体而不将其子实体保存在 asp.net mvc 的 EF6 中?【英文标题】:How to save/update only parent entities without saving its childs entities in EF6 in asp.net mvc? 【发布时间】:2020-04-18 21:54:05 【问题描述】:

我正在使用 Asp.Net MVC 开发一个调查应用程序。

我有一个名为 Index.cshtml 的页面,其中有一个问题表和一个“添加新”按钮。单击按钮后,会使用 jQuery 打开一个弹出窗口。我正在从控制器调用一个视图来填充名为 AddOrEdit.cshtml(子页面)的 jQuery 对话框。我正在添加新的问题和选项。问题是一个文本字段,它的选项被添加到可编辑的表格中。单击提交按钮后,将触发提交表单事件(保存或更新)。我的问题及其选项类具有一对多的关系。 EF6 尝试将父实体与其子实体一起保存。但是我想在插入父母不同的时间后拯救孩子。我该如何处理这个问题。

我正在使用 DB First 方法。最佳做法是什么?

Question.cs

namespace MerinosSurvey.Models

using System;
using System.Collections.Generic;

public partial class Questions

    [System.Diagnostics.CodeAnalysis.SuppressMessage("Microsoft.Usage", "CA2214:DoNotCallOverridableMethodsInConstructors")]
    public Questions()
    
        this.Responses = new HashSet<Responses>();
        this.Options = new HashSet<Options>();
    

    public int QuestionId  get; set; 
    public string QuestionName  get; set; 
    public int QuestionTypeId  get; set; 
    public System.DateTime CreatedDate  get; set; 
    public int CreatedUserId  get; set; 
    public bool IsActive  get; set; 
    public bool Status  get; set; 
    public System.DateTime UpdatedDate  get; set; 

    [System.Diagnostics.CodeAnalysis.SuppressMessage("Microsoft.Usage", "CA2227:CollectionPropertiesShouldBeReadOnly")]
    public virtual ICollection<Responses> Responses  get; set; 
    [System.Diagnostics.CodeAnalysis.SuppressMessage("Microsoft.Usage", "CA2227:CollectionPropertiesShouldBeReadOnly")]
    public virtual ICollection<Options> Options  get; set; 


Option.cs

namespace MerinosSurvey.Models

using System;
using System.Collections.Generic;

public partial class Options

    public int OptionId  get; set; 
    public string OptionName  get; set; 
    public int QuestionId  get; set; 
    public System.DateTime CreatedDate  get; set; 
    public System.DateTime UpdatedDate  get; set; 
    public bool IsActive  get; set; 
    public bool Status  get; set; 

    public virtual Questions Questions  get; set; 


QuestionController.cs - AddOrEdit 操作

    [HttpPost]
    public ActionResult AddOrEdit(Questions question)
    
        if (question != null)
        
            using (MerinosSurveyEntities db = new MerinosSurveyEntities())
            
                Questions questionComing = db.Questions.FirstOrDefault(x => x.QuestionId == question.QuestionId);
                if (questionComing != null)
                
                    //Update
                    questionComing.QuestionName = question.QuestionName;
                    questionComing.Status = true;
                    questionComing.IsActive = true;
                    questionComing.UpdatedDate = DateTime.Now;
                    db.Questions.Attach(questionComing);
                    db.Entry(questionComing).State = EntityState.Modified;
                    question.QuestionId = questionComing.QuestionId;
                    db.SaveChanges();
                
                else
                
                    //New Question
                    question.Status = true;
                    question.IsActive = true;
                    question.UpdatedDate = DateTime.Now;
                    question.CreatedDate = DateTime.Now;
                    db.Questions.Attach(question);
                    db.Entry(question).State = EntityState.Added;
                    db.SaveChanges();
                    question.QuestionId = question.QuestionId;
                

                List<Options> options = question.Options.ToList();
                List<Options> existingOptions = new List<Options>(db.Options.Where(x =>
                    x.Status && x.IsActive && x.QuestionId == question.QuestionId));

                foreach (Options existingOption in existingOptions)
                
                    Options optionUpdated = options.FirstOrDefault(x => x.OptionId == existingOption.OptionId);
                    if (optionUpdated != null)
                    
                        //Update
                        existingOption.UpdatedDate = DateTime.Now;
                        existingOption.OptionName = optionUpdated.OptionName;
                        existingOption.IsActive = true;
                        existingOption.Status = true;
                        db.Options.Attach(existingOption);
                        db.Entry(existingOption).State = EntityState.Modified;
                        db.SaveChanges();
                        options.RemoveAll(x => x.OptionId == existingOption.OptionId);
                    
                    else
                    
                        //Delete
                        existingOption.Status = false;
                        existingOption.UpdatedDate = DateTime.Now;
                        db.Options.Attach(existingOption);
                        db.Entry(existingOption).State = EntityState.Modified;
                        db.SaveChanges();
                    
                

                foreach (Options optionNew in options)
                
                    optionNew.IsActive = true;
                    optionNew.Status = true;
                    optionNew.CreatedDate = DateTime.Now;
                    optionNew.UpdatedDate = DateTime.Now;
                    optionNew.QuestionId = question.QuestionId;
                    db.Options.Add(optionNew);
                    db.SaveChanges();
                

                return Json(new  success = true, message = "Soru başarılı bir şekilde güncellendi." 
  ,
                    JsonRequestBehavior.AllowGet);
            
        

        return Json(new  success = true, message = "Bir problem oluştu." ,
            JsonRequestBehavior.AllowGet);
    

【问题讨论】:

就个人而言,对于我不想更新的任何对象,我都会设置db.Entry(child).State = EntityState.Detached 作为开发人员,我想自己注册子对象。所以首先我保存父母,然后我想保存孩子。但是EF说你不干涉任何事情,我会做这一切,这让我很困扰。这对你来说也一样吗?这种方法是否有效? 当我添加 if($(form).valid()). jquery 验证和引导验证同时工作。 我建议您让 EF 为您完成这项工作,但如果这确实让您感到困扰,因为 Minjack 提到您可以将新添加的子状态设置为 Detached,然后您可以使用 ChangeTracker 获取分离的子实体并手动添加它们 【参考方案1】:

您的方法非常慎重,但容易出现问题。使用 EF,DbContext 的行为很像一个工作单元,并且 SaveChanges 应该只被调用一次。使用诸如相关层次结构之类的东西,其中您有一个带有选项的问题,您可以更新并保存问题,但是如果保存其中一个选项时出现问题会发生什么?您将部分提交更改,并使数据处于不完整、不准确的状态。

这也是很多样板代码,其中一些,例如将跟踪实体的状态显式设置为 Modified 是完全没有必要的。该操作可以修改并简化为:

[HttpPost]
public ActionResult AddOrEdit(Questions question)

    if (question == null) // Assert and fail. Avoids nesting.
        return Json(new  success = true, message = "Bir problem oluştu." ,
            JsonRequestBehavior.AllowGet);

    using (MerinosSurveyEntities db = new MerinosSurveyEntities())
    
        Questions questionComing = db.Questions.Include(x => x.Options).SingleOrDefault(x => x.QuestionId == question.QuestionId); // Prefetch our options...
        if (questionComing != null)
           //Update
            questionComing.QuestionName = question.QuestionName;
            questionComing.Status = true;
            questionComing.IsActive = true;
            questionComing.UpdatedDate = DateTime.Now;
            // db.Questions.Attach(questionComing); -- not needed, already tracked
            // db.Entry(questionComing).State = EntityState.Modified; - Not needed
            // question.QuestionId = questionComing.QuestionId; -- Redundant. The ID matches, we loaded based on it.
            // db.SaveChanges(); -- No save yet.

            // Handle options here... There are probably optimizations that can be added.
            var activeOptionIds = question.Options.Where(x => x.Status && s.IsActive).Select(x => x.OptionId).ToList();
            foreach(var option in question.Options.Where(x => activeOptionIds.Contains(x.OptionId))
            
                var existingOption = questionComing.Options.SingleOrDefault(x => x.OptionId == option.OptionId);
                if(existingOption != null)
                 // Update
                    existingOption.UpdatedDate = DateTime.Now;
                    existingOption.OptionName = optionUpdated.OptionName;
                    existingOption.IsActive = true;
                    existingOption.Status = true;
                
                else
                 // New
                    questionComing.Options.Add(option); // Provided we trust the data coming in. Otherwise new up an option and copy over values.
                
            

            var removedOptions = questionComing.Options.Where(x => !activeOptionIds.Contains(x.OptionId).ToList();
            foreach(var option in removedOptions)
            
                option.Status = option.IsActive = false;
                option.UpdatedDate = DateTime.Now;
             
        
        else
           //New Question
            // Dangerous to trust the Question coming in. Better to validate and copy values to a new Question to add...
            question.Status = true;
            question.IsActive = true;
            question.UpdatedDate = question.CreatedDate = DateTime.Now;

            // db.Questions.Attach(question); -- Add it...
            // db.Entry(question).State = EntityState.Added; 
            // question.QuestionId = question.QuestionId; -- Does nothing...
            db.Questions.Add(question); // This will append all Options as well.
        

        // Now, after all changes are in, Save.
        db.SaveChanges();
        return Json(new  success = true, message = "Soru başarılı bir şekilde güncellendi." ,JsonRequestBehavior.AllowGet);
     // end using.


我将进一步分解为私有方法来处理添加与更新。虽然这并没有回答如何在没有子级的情况下更新父级,但它应该说明为什么您应该利用 EF 的功能来确保子级正确地与父级一起更新。 SaveChanges 只应在 DbContext 的生命周期范围内调用一次,以确保在发生故障时提交或回滚所有相关更改。 EF 管理它被告知要跟踪的实体之间的关系,因此您可以添加具有 new 子级的实体。您需要注意的地方是引用,例如,如果您有一个与新问题相关联的现有 QuestionType 实体。在这些场景中,您总是希望在 DbContext 范围内加载实体并使用该引用,而不是传入的分离引用,因为 EF 会将其视为新实体,从而导致重复数据或重复键约束受到影响。通常建议在客户端和服务器之间传递实体以避免此类问题。如果未正确验证,附加或添加来自客户端的实体可能会使系统暴露于数据篡改,并可能导致引用现有数据时出现问题。

例如,如果您传入一个新问题,该问题的 QuestionType 引用为“MultipleChoice”(问题类型的查找表),其中 QuestionType ID #1。如果您执行以下操作:

db.Questions.Add(question);

“问题”未跟踪,所有引用的实体均未跟踪。如果您将其添加或附加为新实体,则那些引用的实体将被视为新实体。这将有效地插入一个新的 QuestionType ID #1,从而导致键冲突(行已存在),或者插入一个新的 QuestionType ID #12,例如,如果 QuestionType 配置为递增 ID。要解决这个问题:

var existingQuestionType = db.QuestionTypes.Single(x => x.QuestionTypeId == question.QuestionType.QuestionTypeId);
question.QuestionType = existingQuestionType; // Points our question type reference at the existing, tracked reference.
db.Questions.Add(question);

question.QuestionType 和 existingQuestionType 在此示例中的 ID 均为 1。不同之处在于 existingQuestionType 由 context 跟踪/知道,其中 question.QuestionType 是未跟踪的引用。如果我们在上下文不知道引用的情况下添加问题,它会将其视为问题的子记录,并且也希望插入它。这可能是使人们被 EF 引用绊倒并导致问题和努力与相关实体进行更深思熟虑的最大事情之一,但却剥夺了 EF 可以提供的许多优势。我们将新问题引用指向被跟踪实体,因此当 EF 插入问题时,它已经知道 QuestionType 引用是现有行,并且一切都按预期工作。

【讨论】:

以上是关于如何仅保存/更新父实体而不将其子实体保存在 asp.net mvc 的 EF6 中?的主要内容,如果未能解决你的问题,请参考以下文章

使用 EF Core 保存附加实体时如何删除子实体

如何在“详细”实体视图中为“主/父”实体“保存”(核心数据)?

Spring Data Jpa:保存方法仅返回选择,但不执行插入

如何在不更新具有多对多关系的子实体的情况下保留父实体?

持久化实体而不将其附加到 EntityManager

如何在不将数据保存到数据库的情况下更新 Gridview 行