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

Shi*_*mmy 3 c# many-to-many entity-framework foreign-keys ef-code-first

我有一个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");
}
Run Code Online (Sandbox Code Playgroud)

现在有几个business.Categories已经CategoryId设置为现有Category(缺少tho 的Title属性Category).

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

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

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

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

内部异常(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);
}
Run Code Online (Sandbox Code Playgroud)

打电话时SaveChanges,我得到以下内容DbUpdateException:

保存不公开其关系的外键属性的实体时发生错误.EntityEntries属性将返回null,因为无法将单个实体标识为异常的来源.通过在实体类型中公开外键属性,可以更轻松地在保存时处理异常.有关详细信息,请参阅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<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;
    }
  }
}
Run Code Online (Sandbox Code Playgroud)

为了再次说清楚,Category我附加到现有/新的Businesses已经存在于DB中并且有一个ID,这就是我用它来附加它.

Sla*_*uma 5

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

更新现有的业务实体

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

如果你想更新一个分离的对象图 - Business加上Category你的案例中的实体集合- 你通常会遇到这样的问题:集合项可能已从集合中删除而且一个项可能已添加 - 与存储在该集合中的当前状态相比数据库.集合项的属性和父实体也可能Business已被修改.除非您在分离对象图时手动跟踪所有更改 - 即EF本身无法跟踪更改 - 这在Web应用程序中很难,因为您必须在浏览器UI中执行此操作,这是您执行正确UPDATE的唯一机会整个对象图将它与数据库中的当前状态进行比较,然后将对象置于正确的状态Added,DeletedModified(也许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");
}
Run Code Online (Sandbox Code Playgroud)

添加新的业务实体

添加新内容Business及其相关内容的过程Categories是正确的.只需将所有Categories现有实体附加到上下文中,然后将新内容添加Business到上下文中:

foreach (var category in business.Categories)
    Context.Categories.Attach(category);
Context.Businesses.Add(business);
Context.SaveChanges();
Run Code Online (Sandbox Code Playgroud)

如果Categories您所附加的所有内容确实都具有数据库中存在的键值,则该应该可以正常工作.

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

在一个独立的协会的情况下-也就是没有FK属性关联BusinessIdCategory-你这确实OptimisticConcurrencyException.(EF似乎这里假设类别已经从DB由另一个用户被删除.)在一个外键关联的情况下-这是具有FK属性关联BusinessIdCategory-你会得到有关违反外键约束的例外.

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