【问题标题】:How to set a collection-property with FKs?如何使用 FK 设置集合属性?
【发布时间】:2013-06-05 00:58:42
【问题描述】:

我有一个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 设置为现有的CategoryCategoryTitle 属性丢失了)。

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

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

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

保存不为其关系公开外键属性的实体时发生错误。 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);
}

调用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;
    }
  }
}

再次澄清一下,我附加到现有/新 Businesses 的 Category 已经存在于数据库中并且有一个 ID,这就是我用来附加它的 ID。

【问题讨论】:

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


    【解决方案1】:

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

    更新现有业务实体

    这就是您示例中的 if 案例 (if (business.BusinessId &gt; 0))。很明显,这里什么都没有发生,也不会将任何更改存储到数据库中,因为您只是附加了Category 对象和Business 实体,然后调用SaveChanges。附加意味着实体以Unchanged 状态添加到上下文中,对于处于该状态的实体,EF 根本不会向数据库发送任何命令。

    如果您想更新分离的对象图 - 在您的情况下,Business 加上 Category 实体的集合 - 您通常会遇到集合项可能已从集合中删除并且可能已添加项的问题- 与存储在数据库中的当前状态相比。集合项的属性和父实体Business 也可能已被修改。除非您在分离对象图时手动跟踪了所有更改 - 即 EF 本身无法跟踪更改 - 这在 Web 应用程序中很困难,因为您必须在浏览器 UI 中执行此操作,您唯一的机会执行正确的更新整个对象图将其与数据库中的当前状态进行比较,然后将对象置于正确的状态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 具有无效的键值(即它在数据库中不存在)。可能它已在此期间从 DB 中删除,或者因为它没有从 Web UI 正确回发。

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

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

    【讨论】:

    • 感谢您的回复。关于第 1 部分,调用ObjectStateEntrySetValues 是否会更新内部属性(即Categories?),我问是因为图表很大!此外,不应在此页面中编辑类别。我只想用新的类别替换旧的类别,而不考虑旧的类别。
    • @Shimmy: 不,SetValues 只更新Business 的标量属性,而不是导航属性。 “用新类别替换旧类别”到底是什么意思?我的理解是(对于更新案例):您加载业务和类别。然后,用户可以将其他(但现有的)类别分配给业务(复选框?)或从业务中取消分配类别(取消选中复选框?)。对吗?
    • 是的。正是这个。我想将类别设置为当前的复选框,我不在乎之前的内容,应该删除之前的猫。
    • 其实根据我读到的here,当设置一个实体的状态为Modified时,所有的属性都会更新。问题是这是否也适用于引用属性,是吗?
    • @Shimmy:不,它没有。对于您的方案,需要上面的代码或类似的代码。我知道这是很长的代码(如果删除 cmets 会变得更短:))并且比它应该的更复杂,但是缺乏对更新分离图的良好支持是 EF 的一大缺点。此处要求对其进行改进 (entityframework.codeplex.com/workitem/864),但在 EF 6 中仍然没有预期。您还可以尝试在声称支持此功能的工作项(我自己没有尝试过)中链接的 Brent McKendrick 库。跨度>
    【解决方案2】:

    我也有这个例外。 我的问题是添加对象的主键不是由 ADO 实体框架设置的。这导致了数据库中添加对象的外键也无法设置的问题。

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

    希望对你有帮助

    【讨论】:

    • 在我的场景中,PK是100%设置的。
    【解决方案3】:

    我在尝试设置超出其 MaxLength 属性的字段上遇到此异常。该字段还具有必需属性。我正在使用 Oracle 后端数据库。直到我增加了长度,我才最终从底层数据库引擎收到一条描述性错误消息,表明长度太长了。我怀疑与外键相关的错误消息模糊地意味着并非所有关系都可以更新,因为其中一个插入失败并且没有更新它们的键。

    【讨论】:

      猜你喜欢
      • 1970-01-01
      • 2011-04-05
      • 1970-01-01
      • 2014-10-04
      • 1970-01-01
      • 2014-11-03
      • 2015-12-06
      • 2014-07-20
      • 2013-04-15
      相关资源
      最近更新 更多