Как добавить/обновить дочерние сущности при обновлении родительского объекта в EF

Эти два объекта являются отношениями "один ко многим" (созданным с помощью первого кода api).

public class Parent
{
    public Parent()
    {
        this.Children = new List<Child>();
    }

    public int Id { get; set; }

    public virtual ICollection<Child> Children { get; set; }
}

public class Child
{
    public int Id { get; set; }

    public int ParentId { get; set; }

    public string Data { get; set; }
}

В моем контроллере WebApi у меня есть действия для создания родительского объекта (который работает нормально) и обновления родительского объекта (который имеет некоторые проблемы). Действие обновления выглядит так:

public void Update(UpdateParentModel model)
{
    //what should be done here?
}

В настоящее время у меня есть две идеи:

  • Получите отслеживаемый родительский объект с именем existing на model.Id и присвойте значения в model один за другим сущности. Это звучит глупо. И в model.Children я не знаю, какой ребенок является новым, какой ребенок модифицирован (или даже удален).

  • Создайте новый родительский объект через model и привяжите его к DbContext и сохраните его. Но как DbContext знает состояние детей (новое добавление/удаление/изменение)?

Каков правильный способ реализации этой функции?

Ответ 1

Поскольку модель, которая отправляется на контроллер WebApi, отделяется от любого контекста сущности-структуры (EF), единственным вариантом является загрузка графа объекта (родителя, включая его дочерние элементы) из базы данных, и сравнение того, какие дети были добавлены, удалены или обновлены. (Если вы не отслеживаете изменения с помощью собственного механизма отслеживания во время отсоединенного состояния (в браузере или где-либо), что, на мой взгляд, более сложное, чем следующее.) Это может выглядеть так:

public void Update(UpdateParentModel model)
{
    var existingParent = _dbContext.Parents
        .Where(p => p.Id == model.Id)
        .Include(p => p.Children)
        .SingleOrDefault();

    if (existingParent != null)
    {
        // Update parent
        _dbContext.Entry(existingParent).CurrentValues.SetValues(model);

        // Delete children
        foreach (var existingChild in existingParent.Children.ToList())
        {
            if (!model.Children.Any(c => c.Id == existingChild.Id))
                _dbContext.Children.Remove(existingChild);
        }

        // Update and Insert children
        foreach (var childModel in model.Children)
        {
            var existingChild = existingParent.Children
                .Where(c => c.Id == childModel.Id)
                .SingleOrDefault();

            if (existingChild != null)
                // Update child
                _dbContext.Entry(existingChild).CurrentValues.SetValues(childModel);
            else
            {
                // Insert child
                var newChild = new Child
                {
                    Data = childModel.Data,
                    //...
                };
                existingParent.Children.Add(newChild);
            }
        }

        _dbContext.SaveChanges();
    }
}

...CurrentValues.SetValues может принимать любые объекты и отображать значения свойств прикрепленному объекту на основе имени свойства. Если имена свойств в вашей модели отличаются от имен в сущности, вы не можете использовать этот метод и должны присваивать значения один за другим.

Ответ 2

Я возился с чем-то вроде этого...

protected void UpdateChildCollection<Tparent, Tid , Tchild>(Tparent dbItem, Tparent newItem, Func<Tparent, IEnumerable<Tchild>> selector, Func<Tchild, Tid> idSelector) where Tchild : class
    {
        var dbItems = selector(dbItem).ToList();
        var newItems = selector(newItem).ToList();

        if (dbItems == null && newItems == null)
            return;

        var original = dbItems?.ToDictionary(idSelector) ?? new Dictionary<Tid, Tchild>();
        var updated = newItems?.ToDictionary(idSelector) ?? new Dictionary<Tid, Tchild>();

        var toRemove = original.Where(i => !updated.ContainsKey(i.Key)).ToArray();
        var removed = toRemove.Select(i => DbContext.Entry(i.Value).State = EntityState.Deleted).ToArray();

        var toUpdate = original.Where(i => updated.ContainsKey(i.Key)).ToList();
        toUpdate.ForEach(i => DbContext.Entry(i.Value).CurrentValues.SetValues(updated[i.Key]));

        var toAdd = updated.Where(i => !original.ContainsKey(i.Key)).ToList();
        toAdd.ForEach(i => DbContext.Set<Tchild>().Add(i.Value));
    }

который вы можете вызвать с помощью:

UpdateChildCollection(dbCopy, detached, p => p.MyCollectionProp, collectionItem => collectionItem.Id)

К сожалению, этот вид падает, если есть свойства коллекции для дочернего типа, которые также необходимо обновить. Учитывая попытку решить эту проблему, передав IRepository (с базовыми методами CRUD), который будет отвечать за вызов UpdateChildCollection самостоятельно. Вызов репо вместо прямых вызовов в DbContext.Entry.

Не знаю, как все это будет выполняться в масштабе, но не уверен, что еще делать с этой проблемой.

Ответ 3

Если вы используете EntityFrameworkCore, вы можете сделать следующее в своем действии контроллера (метод Attach рекурсивно присоединяет свойства навигации, включая коллекции):

_context.Attach(modelPostedToController);

IEnumerable<EntityEntry> unchangedEntities = _context.ChangeTracker.Entries().Where(x => x.State == EntityState.Unchanged);

foreach(EntityEntry ee in unchangedEntities){
     ee.State = EntityState.Modified;
}

await _context.SaveChangesAsync();

Предполагается, что каждый обновленный объект имеет все свойства, установленные и предоставленные в данных поста от клиента (например, не будет работать для частичного обновления объекта).

Вы также должны убедиться, что вы используете новый/выделенный контекст базы данных платформы сущностей для этой операции.

Ответ 4

Существует несколько проектов, которые облегчают взаимодействие между клиентом и сервером, поскольку это касается сохранения всего графа объектов.

Вот два, на которые вы хотели бы обратить внимание:

Оба вышеприведенных проекта распознают отключенные объекты, когда они возвращаются на сервер, обнаруживают и сохраняют изменения и возвращают данные, затронутые клиентом.

Ответ 5

Хорошо, ребята. Я получил этот ответ один раз, но потерял его по пути. абсолютная пытка, когда ты знаешь лучший способ, но не можешь вспомнить или найти его! Это очень просто. Я только что проверил это несколькими способами.

var parent = _dbContext.Parents
  .Where(p => p.Id == model.Id)
  .Include(p => p.Children)
  .FirstOrDefault();

parent.Children = _dbContext.Children.Where(c => <Query for New List Here>);
_dbContext.Entry(parent).State = EntityState.Modified;

_dbContext.SaveChanges();

Вы можете заменить весь список новым! Код SQL удалит и добавит объекты по мере необходимости. Не нужно беспокоиться об этом. Не забудьте включить детскую коллекцию или не кубики. Удачи!

Ответ 6

Просто доказательство концепции Controler.UpdateModel будет работать неправильно.

Полный класс здесь:

const string PK = "Id";
protected Models.Entities con;
protected System.Data.Entity.DbSet<T> model;

private void TestUpdate(object item)
{
    var props = item.GetType().GetProperties();
    foreach (var prop in props)
    {
        object value = prop.GetValue(item);
        if (prop.PropertyType.IsInterface && value != null)
        {
            foreach (var iItem in (System.Collections.IEnumerable)value)
            {
                TestUpdate(iItem);
            }
        }
    }

    int id = (int)item.GetType().GetProperty(PK).GetValue(item);
    if (id == 0)
    {
        con.Entry(item).State = System.Data.Entity.EntityState.Added;
    }
    else
    {
        con.Entry(item).State = System.Data.Entity.EntityState.Modified;
    }

}

Ответ 7

Попробуйте использовать цикл ForEach, поддерживаемый LINQ в этом случае:

public void Update(ParentModel model, string newData)
{
    var parent = _dbContext.Parents
        .Where(p => p.Id == model.Id)
        .Include(p => p.Children)
        .FirstOrDefault();

    if(parent != null){
       //update parent
         ...
      //update children
      if(parent.Children != null && parent.Children.Any()){
         parent.Children.ForEach(x => x.Data = newData);
      }

   _dbContext.SaveChanges();
}