Как установить свойство коллекции с помощью 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 disposal is taken care of at the controller 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 (свойство Title Category отсутствует).

После нажатия SaveChanges и перезагрузки Business с сервера Categories не существует.

Итак, мой вопрос в том, как правильно установить business.Categories с заданным массивом существующих CategoryId s.

Однако при создании нового Business при вызове SaveChanges:

возникает следующее исключение DbUpdateException

Произошла ошибка при сохранении объектов, которые не отображают свойства внешнего ключа для своих отношений. Свойство EntityEntries вернет значение null, потому что один объект не может быть идентифицирован как источник исключения. Обработка исключений при сохранении может быть упрощена путем отображения свойств внешнего ключа в ваших типах объектов. Подробнее см. В InnerException.

Внутреннее исключение (OptimisticConcurrencyException):

Заявление об обновлении, вставке или удалении повлияло на неожиданное количество строк (0). Объекты могут быть изменены или удалены, поскольку объекты загружены. Обновить записи ObjectStateManager.

Update

после ответа, здесь код обновления:

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.

Вот как выглядят модели Business/Category:

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

Просто, чтобы снова очистить, Category я прикрепляюсь к существующим/новым Business es уже существует в БД и имеет идентификатор, который я использую для присоединения к нему.

Ответ 1

Я рассматриваю два случая - обновление существующего бизнеса и добавление нового бизнеса - отдельно, потому что две упомянутые вами проблемы имеют разные причины.

Обновление существующего бизнес-объекта

Это пример if (if (business.BusinessId > 0)) в вашем примере. Понятно, что здесь ничего не происходит, и никакие изменения не будут сохранены в базе данных, потому что вы просто присоединяете объекты Category и объект Business, а затем вызываете SaveChanges. Прикрепление означает, что сущности добавляются в контекст в состоянии Unchanged, а для объектов, находящихся в этом состоянии, EF вообще не будет отправлять какую-либо команду в базу данных.

Если вы хотите обновить граф отдельных объектов - Business плюс коллекцию объектов Category в вашем случае - вы, как правило, имеете проблему с тем, что элемент коллекции мог быть удален из коллекции, и элемент мог быть добавлен - по сравнению с текущим состоянием, хранящимся в базе данных. Возможно также, что свойства элемента коллекции и что родительский объект Business был изменен. Если вы не отследили все изменения вручную, в то время как граф объекта был отсоединен - ​​то есть сам EF не смог отслеживать изменения - что сложно в веб-приложении, потому что вы должны были сделать это в пользовательском интерфейсе браузера, ваш единственный шанс выполнить правильное 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, которые вы прикрепляете, действительно имеют ключевое значение, существующее в базе данных, это должно работать без исключения.

Ваше исключение означает, что хотя бы один из Categories имеет недопустимое значение ключа (т.е. он не существует в базе данных). Может быть, он был удален тем временем из БД или потому, что он неправильно отправлен обратно из веб-интерфейса.

В случае независимой ассоциации - это ассоциация без свойства FK BusinessId в Category - вы действительно получаете этот OptimisticConcurrencyException. (Предполагается, что EF предполагает, что категория была удалена из БД другим пользователем.) В случае ассоциации внешнего ключа - это ассоциация, которая имеет свойство FK BusinessId в Category - вы получите исключение о нарушении ограничения внешнего ключа.

Если вы хотите избежать этого исключения - и если это происходит на самом деле, потому что другой пользователь удалил Category, а не потому, что Category пуст /0, так как он не отправляется обратно на сервер ( исправьте это со скрытым полем ввода) - вы лучше загружаете категории из CategoryId (Find) из базы данных, а не присоединяете их, а если их больше нет, игнорируйте их и удаляйте из коллекции business.Categories (или перенаправить на страницу с ошибкой, чтобы сообщить пользователю или что-то в этом роде).

Ответ 2

У меня тоже было это исключение. Проблема со мной заключалась в том, что первичный ключ добавленного объекта не был установлен ADO Entity Framework. Это привело к тому, что не удалось установить внешний ключ добавленного объекта в базе данных.

Я решил эту проблему, убедившись, что первичный ключ задается самой базой данных. Если вы используете SQL Server, вы можете это сделать, добавив ключевое слово IDENTITY с объявлением столбца в оператор CREATE TABLE.

Надеюсь, что это поможет

Ответ 3

Я получал это исключение в поле, которое я пытался установить за его атрибут MaxLength. Поле также имеет атрибут Required. Я работаю с базой данных Oracle. Только после того, как я увеличил длину, я наконец получил описательное сообщение об ошибке из базового механизма базы данных, что длина была слишком длинной. Я подозреваю, что сообщение об ошибке, связанное с внешним ключом, смутно означает, что не все отношения могут быть обновлены, потому что одна из вставок не удалась и не обновила их ключ.