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

151

Эти две сущности являются отношением один-ко-многим (строится на основе кода, свободно бегущего 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?
}

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

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

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

Как правильно реализовать эту функцию?

Ченг Чен
источник
См. Также пример с GraphDiff в дублирующем вопросе stackoverflow.com/questions/29351401/…
Майкл

Ответы:

220

Поскольку модель, которая публикуется в контроллере WebApi, отсоединяется от любого контекста Entity Framework (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может принимать любой объект и отображать значения свойств в присоединенную сущность на основе имени свойства. Если имена свойств в вашей модели отличаются от имен в сущности, вы не можете использовать этот метод и должны присваивать значения одно за другим.

Slauma
источник
35
Но почему у ef нет более «блестящего» пути? Я думаю, что ef может определить, был ли ребенок изменен / удален / добавлен, IMO, ваш код выше может быть частью EF и стать более общим решением.
Ченг Чен
7
@DannyChen: Это действительно длинный запрос о том, что EF должно поддерживать обновление отключенных сущностей более удобным способом ( entityframework.codeplex.com/workitem/864 ), но это все еще не является частью фреймворка. В настоящее время вы можете попробовать только стороннюю библиотеку "GraphDiff", которая упоминается в этом рабочем пункте codeplex, или написать ручной код, как в моем ответе выше.
Слаума
7
Одна вещь, которую нужно добавить: в рамках каждого процесса обновления и вставки дочерних элементов вы не можете этого сделать, existingParent.Children.Add(newChild)потому что тогда при поиске существующего Linux linq будет возвращаться недавно добавленная сущность, и эта сущность будет обновлена. Вам просто нужно вставить во временный список, а затем добавить.
Erre Efe
3
@ RandolfRincónFadul Я только что столкнулся с этой проблемой. Мое исправление, которое existingChild.Where(c => c.ID == childModel.ID && c.ID != default(int))
Гэвин Уорд
2
@RalphWillgoss О каком исправлении в 2.2 вы говорили?
Ян Паоло Go
11

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

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.

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

brettman
источник
1
Отличное решение! Но не удается, если добавить более одного нового элемента, обновленный словарь не может иметь нулевой идентификатор дважды. Нужна работа вокруг. И также не работает, если отношение N -> N, фактически элемент добавляется в базу данных, но таблица N -> N не изменяется.
RenanStr
1
toAdd.ForEach(i => (selector(dbItem) as ICollection<Tchild>).Add(i.Value));должен решить n -> n проблему.
RenanStr
10

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

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 будет удалять и добавлять объекты по мере необходимости. Не нужно беспокоиться об этом. Не забудьте включить детскую коллекцию или не кубики. Удачи!

Чарльз Макинтош
источник
Как раз то, что мне нужно, так как число дочерних элементов в моей модели, как правило, довольно мало, поэтому, если предположить, что Linq сначала удалит все исходные дочерние элементы из таблицы, а затем добавит всех новых, влияние на производительность не является проблемой.
Уильям Т. Маллард
@ Чарльз Макинтош. Я не понимаю, почему вы снова устанавливаете Children, когда включаете его в первоначальный запрос?
пантонис
1
@pantonis Я включаю дочернюю коллекцию, чтобы ее можно было загрузить для редактирования. Если я полагаюсь на ленивую загрузку, чтобы понять это, это не сработает. Я установил дочерние элементы (один раз), потому что вместо того, чтобы вручную удалять и добавлять элементы в коллекцию, я могу просто заменить список, а объектная структура добавит и удалит элементы для меня. Ключевым моментом является установка состояния объекта в измененное состояние и разрешение каркасу объекта выполнять тяжелую работу.
Чарльз Макинтош
@CharlesMcIntosh Я до сих пор не понимаю, чего вы пытаетесь достичь с детьми там. Вы включили его в первый запрос (Include (p => p.Children). Почему вы запрашиваете его снова?
pantonis
@pantonis, мне пришлось вытащить старый список с помощью .include (), чтобы он загружался и прикреплялся как коллекция из базы данных. Это как ленивая загрузка вызывается. без него любые изменения в списке не будут отслеживаться, когда я использовал entitystate.modified. повторяю, что я делаю, устанавливая текущую дочернюю коллекцию в другую дочернюю коллекцию. например, если менеджер получил кучу новых сотрудников или потерял несколько. Я бы использовал запрос, чтобы включить или исключить этих новых сотрудников и просто заменить старый список новым списком, а затем позволить EF добавлять или удалять по мере необходимости со стороны базы данных.
Чарльз Макинтош
9

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

_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();

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

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

hallz
источник
5
public async Task<IHttpActionResult> PutParent(int id, Parent parent)
        {
            if (!ModelState.IsValid)
            {
                return BadRequest(ModelState);
            }

            if (id != parent.Id)
            {
                return BadRequest();
            }

            db.Entry(parent).State = EntityState.Modified;

            foreach (Child child in parent.Children)
            {
                db.Entry(child).State = child.Id == 0 ? EntityState.Added : EntityState.Modified;
            }

            try
            {
                await db.SaveChangesAsync();
            }
            catch (DbUpdateConcurrencyException)
            {
                if (!ParentExists(id))
                {
                    return NotFound();
                }
                else
                {
                    throw;
                }
            }

            return Ok(db.Parents.Find(id));
        }

Вот как я решил эту проблему. Таким образом, EF знает, что добавить, а что обновить.

Jokeur
источник
Работал как шарм! Спасибо.
Inktkiller
2

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

Вот два, на которые вы хотели бы взглянуть:

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

Шимми Вайцхандлер
источник
1

Просто доказательство концепции 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;
    }

}
Mertuarez
источник
0

@ Чарльз Макинтош действительно дал мне ответ для моей ситуации в том, что переданная модель была отсоединена. Для меня то, что в конечном итоге сработало, это сначала сохранить переданную модель ... затем продолжить добавлять детей, как я уже был раньше:

public async Task<IHttpActionResult> GetUPSFreight(PartsExpressOrder order)
{
    db.Entry(order).State = EntityState.Modified;
    db.SaveChanges();
  ...
}
Энтони Григгс
источник
0

Для разработчиков VB.NET Используйте этот универсальный саб, чтобы отметить дочернее состояние, простое в использовании

Ноты:

  • PromatCon: объект сущности
  • amList: дочерний список, который вы хотите добавить или изменить
  • rList: дочерний список, который вы хотите удалить
updatechild(objCas.ECC_Decision, PromatCon.ECC_Decision.Where(Function(c) c.rid = objCas.rid And Not objCas.ECC_Decision.Select(Function(x) x.dcid).Contains(c.dcid)).toList)
Sub updatechild(Of Ety)(amList As ICollection(Of Ety), rList As ICollection(Of Ety))
        If amList IsNot Nothing Then
            For Each obj In amList
                Dim x = PromatCon.Entry(obj).GetDatabaseValues()
                If x Is Nothing Then
                    PromatCon.Entry(obj).State = EntityState.Added
                Else
                    PromatCon.Entry(obj).State = EntityState.Modified
                End If
            Next
        End If

        If rList IsNot Nothing Then
            For Each obj In rList.ToList
                PromatCon.Entry(obj).State = EntityState.Deleted
            Next
        End If
End Sub
PromatCon.SaveChanges()
Бэзил
источник
0
var parent = context.Parent.FirstOrDefault(x => x.Id == modelParent.Id);
if (parent != null)
{
  parent.Childs = modelParent.Childs;
}

источник

Alex
источник
0

Вот мой код, который работает просто отлично.

public async Task<bool> UpdateDeviceShutdownAsync(Guid id, DateTime shutdownAtTime, int areaID, decimal mileage,
        decimal motohours, int driverID, List<int> commission,
        string shutdownPlaceDescr, int deviceShutdownTypeID, string deviceShutdownDesc,
        bool isTransportation, string violationConditions, DateTime shutdownStartTime,
        DateTime shutdownEndTime, string notes, List<Guid> faultIDs )
        {
            try
            {
                using (var db = new GJobEntities())
                {
                    var isExisting = await db.DeviceShutdowns.FirstOrDefaultAsync(x => x.ID == id);

                    if (isExisting != null)
                    {
                        isExisting.AreaID = areaID;
                        isExisting.DriverID = driverID;
                        isExisting.IsTransportation = isTransportation;
                        isExisting.Mileage = mileage;
                        isExisting.Motohours = motohours;
                        isExisting.Notes = notes;                    
                        isExisting.DeviceShutdownDesc = deviceShutdownDesc;
                        isExisting.DeviceShutdownTypeID = deviceShutdownTypeID;
                        isExisting.ShutdownAtTime = shutdownAtTime;
                        isExisting.ShutdownEndTime = shutdownEndTime;
                        isExisting.ShutdownStartTime = shutdownStartTime;
                        isExisting.ShutdownPlaceDescr = shutdownPlaceDescr;
                        isExisting.ViolationConditions = violationConditions;

                        // Delete children
                        foreach (var existingChild in isExisting.DeviceShutdownFaults.ToList())
                        {
                            db.DeviceShutdownFaults.Remove(existingChild);
                        }

                        if (faultIDs != null && faultIDs.Any())
                        {
                            foreach (var faultItem in faultIDs)
                            {
                                var newChild = new DeviceShutdownFault
                                {
                                    ID = Guid.NewGuid(),
                                    DDFaultID = faultItem,
                                    DeviceShutdownID = isExisting.ID,
                                };

                                isExisting.DeviceShutdownFaults.Add(newChild);
                            }
                        }

                        // Delete all children
                        foreach (var existingChild in isExisting.DeviceShutdownComissions.ToList())
                        {
                            db.DeviceShutdownComissions.Remove(existingChild);
                        }

                        // Add all new children
                        if (commission != null && commission.Any())
                        {
                            foreach (var cItem in commission)
                            {
                                var newChild = new DeviceShutdownComission
                                {
                                    ID = Guid.NewGuid(),
                                    PersonalID = cItem,
                                    DeviceShutdownID = isExisting.ID,
                                };

                                isExisting.DeviceShutdownComissions.Add(newChild);
                            }
                        }

                        await db.SaveChangesAsync();

                        return true;
                    }
                }
            }
            catch (Exception ex)
            {
                logger.Error(ex);
            }

            return false;
        }
разработчик
источник