我用Northwind数据库做了一个小项目来说明问题。
以下是控制器的操作:
[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();
}
}
上下文在Controller的构造函数中以new DatabaseContext()
实例化。
public class DatabaseContext:DbContext
{
public DatabaseContext()
: base("ApplicationServices") {
base.Configuration.ProxyCreationEnabled = false;
base.Configuration.LazyLoadingEnabled = false;
}
public DbSet<Product> Products { get; set; }
public DbSet<Category> Categories { get; set; }
protected override void OnModelCreating(DbModelBuilder modelBuilder){
modelBuilder.Configurations.Add(new ProductConfiguration());
modelBuilder.Configurations.Add(new CategoriesConfiguration());
}
private class ProductConfiguration : EntityTypeConfiguration<Product> {
public ProductConfiguration() {
ToTable("Products");
HasKey(p => p.ProductID);
HasOptional(p => p.Category).WithMany(x=>x.Products).Map(c => c.MapKey("CategoryID"));
Property(p => p.UnitPrice).HasColumnType("Money");
}
}
private class CategoriesConfiguration : EntityTypeConfiguration<Category> {
public CategoriesConfiguration() {
ToTable("Categories");
HasKey(p => p.CategoryID);
}
}
}
public class Category {
public int CategoryID { get; set; }
public string CategoryName { get; set; }
public string Description { get; set; }
public virtual ICollection<Product> Products { get; set; }
}
public class Product {
public int ProductID { get; set; }
public string ProductName { get; set; }
public string QuantityPerUnit { get; set; }
public decimal UnitPrice { get; set; }
public Int16 UnitsInStock { get; set; }
public Int16 UnitsOnOrder { get; set; }
public Int16 ReorderLevel { get; set; }
public bool Discontinued { get; set; }
public virtual Category Category { get; set; }
}
问题是我可以保存产品中的任何内容,但不能保存类别的更改。
对象productFromForm在productFromForm.Product.ProductID中包含新的CategoryID而没有任何问题。但是,当我Find()
从上下文中检索对象的类别时,我有一个没有Name和Description的对象(都保持为NULL)并且SaveChanges()
不修改引用,即使ID有已更改为属性Category
。
知道为什么吗?
答案 0 :(得分:7)
你(显然)改变的关系没有得到保存,因为你没有真正改变关系:
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();
如果将外键作为模型中的属性公开,则会变得容易得多 - 正如@Laniency的答案和前一个问题的答案中所述。使用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();
答案 1 :(得分:2)
问题在于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();
}
我发现的最不容易出错的解决方案,特别是模型变得更复杂,有两个方面:
在域对象中明确声明外键。即,添加一个CategoryId做你的产品。将输入映射到此属性,而不是关联对象。 Ladislav's answer和subsequent post详细解释了这一点。独立协会和外键都有自己的问题,但到目前为止,我发现外键方法没有头痛(即关联实体被标记为添加,附加顺序,在映射之前跨越数据库问题等等... )
public class Product
{
// EF will automatically assume FooId is the foreign key for Foo.
// When mapping input, change this one, not the associated object.
[Required]
public int CategoryId { get; set; }
public virtual Category Category { get; set; }
}