如何使用FK设置集合属性?
我有一个Business
和Category
模型。
每个Business
通过公开的集合拥有许多Categories
( Category
无视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
设置为现有Category
( Category
的Title
属性缺失)。
点击SaveChanges
并从服务器重新加载Business
后, Categories
不存在。
所以我的问题是用给定数组的现有CategoryId
设置Business.Categories
的正确方法是什么。
但是,在创建新Business
时,调用SaveChanges
时会抛出以下DbUpdateException
exception:
保存不公开其关系的外键属性的实体时发生错误。 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; } } }
为了再次说清楚,我附加到现有/新Business
的Category
I已经存在于数据库中并且具有ID,这是我用来附加它的ID。
我将这两种情况 – 更新现有业务和添加新业务 – 分开处理,因为您提到的两个问题有不同的原因。
更新现有业务实体
这是你的例子中的if
case( if (business.BusinessId > 0)
)。 很明显,此处没有任何操作,并且不会将更改存储到数据库中,因为您只是附加了Category
对象和Business
实体,然后调用SaveChanges
。 附加意味着实体被添加到状态Unchanged
的上下文中,对于处于该状态的实体,EF根本不会向数据库发送任何命令。
如果要更新分离的对象图 – Business
加上您的案例中的Category
实体集合 – 您通常会遇到这样的问题:集合项可能已从集合中删除并且项目可能已添加 – 与存储的当前状态相比在数据库中。 集合项的属性和父实体Business
也可能已被修改。 除非您在分离对象图时手动跟踪所有更改 – 即EF本身无法跟踪更改 – 这在Web应用程序中很难,因为您必须在浏览器UI中执行此操作,这是您执行正确UPDATE的唯一机会整个对象图将它与数据库中的当前状态进行比较,然后将对象置于正确的状态: Added
, Deleted
和Modified
(对于其中一些状态可能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
因为它没有被发回服务器(用隐藏的输入字段来修复它) – 您最好通过CategoryId
( Find
)从数据库加载类别而不是附加它们,如果不存在则忽略它并将其从business.Categories
集合中删除(或重定向到错误页面以通知用户或其他东西)像那样)。
我也有这个例外。 我的问题是ADO Entity Framework没有设置添加对象的主键。 这导致了无法设置数据库中添加的对象的外键的问题。
我通过确保主键由数据库本身设置来解决了这个问题。 如果您正在使用SQL Server,则可以通过在CREATE TABLE语句中添加IDENTITY关键字和列声明来实现。
希望这可以帮助
我正在尝试设置超出其MaxLength属性的字段上获得此exception。 该字段还具有Required属性。 我正在攻击Oracle后端数据库。 直到我增加了长度,我终于从底层数据库引擎得到一个描述性错误消息,长度太长。 我怀疑外键相关的错误消息含糊地意味着并非所有关系都可以更新,因为其中一个插入失败并且未更新其密钥。