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

时间:2013-06-09 02:19:04

标签: c# entity-framework ef-code-first foreign-keys many-to-many

我有一个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.CategoriesCategoryId设置为现有CategoryTitle的{​​{1}}属性缺失了。)

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

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

但是,在创建新的CategoryId时,在调用Business时会抛出以下DbUpdateException异常:

  

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

内部异常(SaveChanges):

  

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

更新

回答后,这是更新代码:

OptimisticConcurrencyException

致电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

  

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

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

DbUpdateException

为了再次说清楚,我附加到现有/新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<Category> _Categories; public virtual ICollection<Category> Categories { get { return _Categories ?? (_Categories = new HashSet<Category>()); } set { _Categories = value; } } private ICollection<Branch> _Branches; public virtual ICollection<Branch> Branches { get { return _Branches ?? (_Branches = new HashSet<Branch>()); } 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<Category> _Children; public virtual ICollection<Category> Children { get { return _Children ?? (_Children = new HashSet<Category>()); } set { _Children = value; } } } es的Category已经存在于数据库中,并且有一个ID,我正在使用它来附加它用。

3 个答案:

答案 0 :(得分:5)

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

更新现有业务实体

您的示例中的if案例(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确实都有一个存在于数据库中的键值,那么这应该毫无例外地工作。

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

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

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

答案 1 :(得分:0)

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

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

希望这有帮助

答案 2 :(得分:0)

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