如何使用FK设置集合属性?

我有一个BusinessCategory模型。

每个Business通过公开的集合拥有许多CategoriesCategory无视Business实体)。

现在这是我的控制器动作:

 [HttpPost] [ValidateAntiForgeryToken] private ActionResult Save(Business business) { //Context is a lazy-loaded property that returns a reference to the DbContext //It's disposal is taken care of at the controller's Dispose override. foreach (var category in business.Categories) Context.Categories.Attach(category); if (business.BusinessId > 0) Context.Businesses.Attach(business); else Context.Businesses.Add(business); Context.SaveChanges(); return RedirectToAction("Index"); } 

现在有几个business.Categories将其CategoryId设置为现有CategoryCategoryTitle属性缺失)。

点击SaveChanges并从服务器重新加载Business后, Categories不存在。

所以我的问题是用给定数组的现有CategoryId设置Business.Categories的正确方法是什么。

但是,在创建新Business时,调用SaveChanges时会抛出以下DbUpdateExceptionexception:

保存不公开其关系的外键属性的实体时发生错误。 EntityEntries属性将返回null,因为无法将单个实体标识为exception的来源。 通过在实体类型中公开外键属性,可以更轻松地在保存时处理exception。 有关详细信息,请参阅InnerException。

内部exception( OptimisticConcurrencyException ):

存储更新,插入或删除语句会影响意外的行数(0)。 自实体加载后,实体可能已被修改或删除。 刷新ObjectStateManager条目。

更新

回答后,这是更新代码:

 var storeBusiness = IncludeChildren().SingleOrDefault(b => b.BusinessId == business.BusinessId); var entry = Context.Entry(storeBusiness); entry.CurrentValues.SetValues(business); //storeBusiness.Categories.Clear(); foreach (var category in business.Categories) { Context.Categories.Attach(category); storeBusiness.Categories.Add(category); } 

调用SaveChanges ,我收到以下DbUpdateException

保存不公开其关系的外键属性的实体时发生错误。 EntityEntries属性将返回null,因为无法将单个实体标识为exception的来源。 通过在实体类型中公开外键属性,可以更轻松地在保存时处理exception。 有关详细信息,请参阅InnerException。

以下是业务/类别模型的外观:

 public class Business { public int BusinessId { get; set; } [Required] [StringLength(64)] [Display(Name = "Company name")] public string CompanyName { get; set; } public virtual BusinessType BusinessType { get; set; } private ICollection _Categories; public virtual ICollection Categories { get { return _Categories ?? (_Categories = new HashSet()); } set { _Categories = value; } } private ICollection _Branches; public virtual ICollection Branches { get { return _Branches ?? (_Branches = new HashSet()); } set { _Branches = value; } } } public class Category { [Key] public int CategoryId { get; set; } [Unique] [Required] [MaxLength(32)] public string Title { get; set; } public string Description { get; set; } public int? ParentCategoryId { get; set; } [Display(Name = "Parent category")] [ForeignKey("ParentCategoryId")] public virtual Category Parent { get; set; } private ICollection _Children; public virtual ICollection Children { get { return _Children ?? (_Children = new HashSet()); } set { _Children = value; } } } 

为了再次说清楚,我附加到现有/新BusinessCategory I已经存在于数据库中并且具有ID,这是我用来附加它的ID。

我将这两种情况 – 更新现有业务和添加新业务 – 分开处理,因为您提到的两个问题有不同的原因。

更新现有业务实体

这是你的例子中的if case( if (business.BusinessId > 0) )。 很明显,此处没有任何操作,并且不会将更改存储到数据库中,因为您只是附加了Category对象和Business实体,然后调用SaveChanges 。 附加意味着实体被添加到状态Unchanged的上下文中,对于处于该状态的实体,EF根本不会向数据库发送任何命令。

如果要更新分离的对象图 – Business加上您的案例中的Category实体集合 – 您通常会遇到这样的问题:集合项可能已从集合中删除并且项目可能已添加 – 与存储的当前状态相比在数据库中。 集合项的属性和父实体Business也可能已被修改。 除非您在分离对象图时手动跟踪所有更改 – 即EF本身无法跟踪更改 – 这在Web应用程序中很难,因为您必须在浏览器UI中执行此操作,这是您执行正确UPDATE的唯一机会整个对象图将它与数据库中的当前状态进行比较,然后将对象置于正确的状态: AddedDeletedModified (对于其中一些状态可能Unchanged )。

因此,该过程是从数据库加载Business及其当前Categories ,然后将分离图的更改合并到已加载(=附加)的图中。 它可能看起来像这样:

 private ActionResult Save(Business business) { if (business.BusinessId > 0) // = business exists { var businessInDb = Context.Businesses .Include(b => b.Categories) .Single(b => b.BusinessId == business.BusinessId); // Update parent properties (only the scalar properties) Context.Entry(businessInDb).CurrentValues.SetValues(business); // Delete relationship to category if the relationship exists in the DB // but has been removed in the UI foreach (var categoryInDb in businessInDb.Categories.ToList()) { if (!business.Categories.Any(c => c.CategoryId == categoryInDb.CategoryId)) businessInDb.Categories.Remove(categoryInDb); } // Add relationship to category if the relationship doesn't exist // in the DB but has been added in the UI foreach (var category in business.Categories) { var categoryInDb = businessInDb.Categories.SingleOrDefault(c => c.CategoryId == category.CategoryId) if (categoryInDb == null) { Context.Categories.Attach(category); businessInDb.Categories.Add(category); } // no else case here because I assume that categories couldn't have // have been modified in the UI, otherwise the else case would be: // else // Context.Entry(categoryInDb).CurrentValues.SetValues(category); } } else { // see below } Context.SaveChanges(); return RedirectToAction("Index"); } 

添加新的业务实体

您添加新Business及其相关Categories是正确的。 只需将所有Categories作为现有实体附加到上下文中,然后将新Business添加到上下文中:

 foreach (var category in business.Categories) Context.Categories.Attach(category); Context.Businesses.Add(business); Context.SaveChanges(); 

如果您附加的所有Categories确实都具有数据库中存在的键值,则此应该无exception地起作用。

您的exception意味着至少有一个Categories具有无效的键值(即它在数据库中不存在)。 也许它在同时从数据库中被删除,或者因为它没有从Web UI正确发回。

如果是一个独立的关联 – 这是一个没有Category FK属性BusinessId的关联 – 你确实会得到这个OptimisticConcurrencyException 。 (EF似乎在这里假设该类别已被其他用户从数据库中删除。)如果是外键关联 – 这是一个在Category具有FK属性BusinessId的关联 – 您将获得有关外键的例外约束违规。

如果你想避免这种exception – 如果它实际上是因为另一个用户删除了一个Category ,而不是因为Category为空/ 0因为它没有被发回服务器(用隐藏的输入字段来修复它) – 您最好通过CategoryIdFind )从数据库加载类别而不是附加它们,如果不存在则忽略它并将其从business.Categories集合中删除(或重定向到错误页面以通知用户或其他东西)像那样)。

我也有这个例外。 我的问题是ADO Entity Framework没有设置添加对象的主键。 这导致了无法设置数据库中添加的对象的外键的问题。

我通过确保主键由数据库本身设置来解决了这个问题。 如果您正在使用SQL Server,则可以通过在CREATE TABLE语句中添加IDENTITY关键字和列声明来实现。

希望这可以帮助

我正在尝试设置超出其MaxLength属性的字段上获得此exception。 该字段还具有Required属性。 我正在攻击Oracle后端数据库。 直到我增加了长度,我终于从底层数据库引擎得到一个描述性错误消息,长度太长。 我怀疑外键相关的错误消息含糊地意味着并非所有关系都可以更新,因为其中一个插入失败并且未更新其密钥。