C# 实体框架4.3,MVC处于编辑状态';无法保存复杂对象
我用Northwind数据库做了一个小项目来说明这个问题 以下是控制器的操作:C# 实体框架4.3,MVC处于编辑状态';无法保存复杂对象,c#,asp.net-mvc,asp.net-mvc-3,entity-framework,entity-framework-4.3,C#,Asp.net Mvc,Asp.net Mvc 3,Entity Framework,Entity Framework 4.3,我用Northwind数据库做了一个小项目来说明这个问题 以下是控制器的操作: [HttpPost] public ActionResult Edit(Product productFromForm) { try { context.Products.Attach(productFromForm); var fromBD = context.Categories.Find(productFromForm.Category.CategoryID);
[HttpPost]
public ActionResult Edit(Product productFromForm)
{
try
{
context.Products.Attach(productFromForm);
var fromBD = context.Categories.Find(productFromForm.Category.CategoryID);
productFromForm.Category = fromBD;
context.Entry(productFromForm).State = EntityState.Modified;
context.SaveChanges();
return RedirectToAction("Index");
}
catch
{
return View();
}
}
上下文在控制器的构造函数中实例化为newdatabasecontext()
公共类数据库上下文:DbContext
{
公共数据库上下文()
:base(“应用程序服务”){
base.Configuration.ProxyCreationEnabled=false;
base.Configuration.LazyLoadingEnabled=false;
}
公共数据库集产品{get;set;}
公共数据库集类别{get;set;}
模型创建时受保护的覆盖无效(DbModelBuilder modelBuilder){
modelBuilder.Configurations.Add(新产品配置());
modelBuilder.Configurations.Add(新类别配置());
}
私有类ProductConfiguration:EntityTypeConfiguration{
公共产品配置(){
ToTable(“产品”);
HasKey(p=>p.ProductID);
has可选(p=>p.Category)。有许多(x=>x.Products)。Map(c=>c.MapKey(“CategoryID”);
房地产(p=>p.UnitPrice).HasColumnType(“货币”);
}
}
私有类类别配置:EntityTypeConfiguration{
公共类别配置(){
ToTable(“类别”);
HasKey(p=>p.CategoryID);
}
}
}
公共类类别{
public int CategoryID{get;set;}
公共字符串CategoryName{get;set;}
公共字符串说明{get;set;}
公共虚拟ICollection产品{get;set;}
}
公共类产品{
public int ProductID{get;set;}
公共字符串ProductName{get;set;}
公共字符串QuantityPerUnit{get;set;}
公共十进制单价{get;set;}
public Int16 UnitsInStock{get;set;}
公共Int16 UnitsOrder{get;set;}
公共Int16重新排序级别{get;set;}
公共布尔已中断{get;set;}
公共虚拟类别{get;set;}
}
问题是我可以保存产品中的任何内容,但不能保存类别的更改
对象productFromForm包含productFromForm.Product.ProductID中的新CategoryID,没有问题。但是,当我Find()
从上下文中检索对象的类别时,我有一个没有名称和描述的对象(两者都保持为NULL),并且SaveChanges()
即使属性类别的ID已更改,也不会修改引用
知道为什么吗?问题是EF跟踪关联更新的方式不同于值类型。执行此操作时,context.Products.Attach(productFromForm)代码>,productFromForm只是一个不跟踪任何更改的poco。当您将其标记为已修改时,EF将更新所有值类型,但不会更新关联
更常见的方法是:
[HttpPost]
public ActionResult Edit(Product productFromForm)
{
// Might need this - category might get attached as modified or added
context.Categories.Attach(productFromForm.Category);
// This returns a change-tracking proxy if you have that turned on.
// If not, then changing product.Category will not get tracked...
var product = context.Products.Find(productFromForm.ProductId);
// This will attempt to do the model binding and map all the submitted
// properties to the tracked entitiy, including the category id.
if (TryUpdateModel(product)) // Note! Vulnerable to overposting attack.
{
context.SaveChanges();
return RedirectToAction("Index");
}
return View();
}
我发现的最不容易出错的解决方案有两个方面,尤其是当模型变得更复杂时:
- 对任何输入使用DTO(类ProductInput)。然后使用AutoMapper之类的工具将数据映射到域对象。在您开始提交越来越复杂的数据时尤其有用
- 在域对象中显式声明外键。例如,添加一个类别来制作您的产品。将输入映射到此属性,而不是关联对象。并进一步解释这一点。独立关联和外键都有各自的问题,但到目前为止,我发现外键方法没有那么麻烦(例如,关联实体标记为已添加、连接顺序、映射前跨数据库关注点等)
您(显然)更改的关系不会被保存,因为您没有真正更改关系:
context.Products.Attach(productFromForm);
此行将productFromForm
和productFromForm.Category
附加到上下文
var fromBD = context.Categories.Find(productFromForm.Category.CategoryID);
此行返回附加的对象productFromForm.Category
,而不是数据库中的对象
productFromForm.Category = fromBD;
这条线指定了同一个对象,因此它什么也不做
context.Entry(productFromForm).State = EntityState.Modified;
此行仅影响productFromForm
的标量属性,而不影响任何导航属性
更好的办法是:
// Get original product from DB including category
var fromBD = context.Products
.Include(p => p.Category) // necessary because you don't have a FK property
.Single(p => p.ProductId == productFromForm.ProductId);
// Update scalar properties of product
context.Entry(fromBD).CurrentValues.SetValues(productFromForm);
// Update the Category reference if the CategoryID has been changed in the from
if (productFromForm.Category.CategoryID != fromBD.Category.CategoryID)
{
context.Categories.Attach(productFromForm.Category);
fromBD.Category = productFromForm.Category;
}
context.SaveChanges();
如果您在模型中将外键作为属性公开,这将变得容易得多——正如@Lenence的答案和您之前问题的答案中所述。使用FK属性(并假设您将Product.CategoryID
直接绑定到视图,而不是Product.Category.CategoryID
),上述代码简化为:
var fromBD = context.Products
.Single(p => p.ProductId == productFromForm.ProductId);
context.Entry(fromBD).CurrentValues.SetValues(productFromForm);
context.SaveChanges();
或者,您可以将状态设置为Modified
,这将适用于FK属性:
context.Entry(productFromForm).State = EntityState.Modified;
context.SaveChanges();
如果您只是设置product.CategoryID而不是类别导航引用,会发生什么情况?productFromForm已经包含表单中正确填写的.CategoryID。啊,我没有直接在控制器中使用我的实体,所以我不确定。谢谢。目前,我们决定不使用外键之类的数据库内容来修改模型。我知道编写代码要少一些,但这是团队目前的决定。这个解决方案很有效,解释也很可靠。干得好您认为将0..1关系的类别设置为NULL是否适用于此类代码?@PatrickDesjardins:是的,我认为它会起作用。在将引用设置为null之前,您只需使用Include
加载原始文件。我做了一个测试,它也可以工作。非常感谢您的回答和它的质量。
var fromBD = context.Products
.Single(p => p.ProductId == productFromForm.ProductId);
context.Entry(fromBD).CurrentValues.SetValues(productFromForm);
context.SaveChanges();
context.Entry(productFromForm).State = EntityState.Modified;
context.SaveChanges();