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


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.

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


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

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

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


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 знает, что добавить, а что обновить.


Работал как шарм! Спасибо.
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;
    }

}

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

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

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;
        }
Используя наш сайт, вы подтверждаете, что прочитали и поняли нашу Политику в отношении файлов cookie и Политику конфиденциальности.
Licensed under cc by-sa 3.0 with attribution required.