Як додати / оновити дочірні сутності під час оновлення материнської сутності в 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 / ...
Michael Freidgeim

Відповіді:


219

Оскільки модель, яка розміщується на контролері 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може приймати будь-який об’єкт і відображає значення властивостей доданому об'єкту на основі імені властивості. Якщо імена властивостей у вашій моделі відрізняються від імен у сутності, ви не можете використовувати цей метод і повинні призначити значення по одному.


35
Але чому ef не має більш "блискучого" способу? Я думаю, що ef може виявити, чи дитина модифікована / видалена / додана, IMO ваш код вище може стати частиною рамки EF та стати більш загальним рішенням.
Чен Чен

7
@DannyChen: Це дійсно довгий запит про те, що оновлення відключених об'єктів має підтримуватися EF більш зручним способом ( entitframework.codeplex.com/workitem/864 ), але це все ще не входить в рамки. Наразі ви можете спробувати лише сторонній посібник "GraphDiff", який згадується в тій роботі кодексу, або написати ручний код, як у моїй відповіді вище.
Слаума

7
Ще одне, що потрібно додати: в рамках передбачення оновлення та вставки дітей ви не можете цього робити, existingParent.Children.Add(newChild)тому що існуючий пошук пошуку по каналу linq поверне нещодавно додане об'єкт, і таким чином сутність буде оновлена. Вам просто потрібно вставити у тимчасовий список, а потім додати.
Ерре Ефе

3
@ RandolfRincónFadul Я просто натрапив на це питання. Моє виправлення, яке трохи менше зусиль, полягає в тому, щоб змінити пункт де у existingChildзапиті LINQ:.Where(c => c.ID == childModel.ID && c.ID != default(int))
Gavin Ward,

2
@RalphWillgoss Що таке виправлення в 2.2, про яке ви говорили?
Ян Паоло Іди

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 спочатку видалить усіх оригінальних дітей із таблиці, а потім додасть усіх нових, вплив на продуктивність не є проблемою.
Вільям Т. Маллард

@Charles McIntosh. Я не зупиняюсь, чому ви знову встановлюєте Дітей, коли ви включаєте їх у початковий запит?
пантоніс

1
@pantonis Я включаю дочірню колекцію, щоб її можна було завантажити для редагування. Якщо я покладаюся на ледачий навантаження, щоб зрозуміти, що це не працює. Я встановлюю дітей (один раз), тому що замість видалення та додавання елементів до колекції вручну я можу просто замінити список, а суб’єктні рамки додаватимуть і видалятимуть елементи для мене. Ключовим моментом є налаштування стану суб'єкта господарювання на модифікованому режимі та дозволення суб'єктам кадрів робити важкі підйоми.
Чарльз Макінтош

@CharlesMcIntosh Я все ще не розумію, чого ти намагаєшся досягти з дітьми там. Ви включили його в перший запит (Включити (p => p.Children). Чому ви знову це запитуєте?
pantonis

@pantonis, мені довелося витягнути старий список, використовуючи .include (), щоб він завантажився і додався як колекція з бази даних. Ось як викликається ледаче завантаження. без цього будь-які зміни у списку не відслідковуватимуться, коли я використовував entitstate.modified. щоб ще раз зазначити, що я роблю, це встановити поточну дитячу колекцію на іншу дитячу колекцію. наприклад, якщо менеджер отримав купу нових співробітників або втратив кількох. Я б використовував запит, щоб включити або виключити цих нових співробітників і просто замінити старий список на новий, а потім дозволити EF додавати або видаляти за потребою зі сторони бази даних.
Чарльз Макінтош,

9

Якщо ви використовуєте 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();

Передбачається, що кожне оновлене об'єднання має всі властивості, встановлені та надані у даних про пост від клієнта (наприклад, не буде працювати для часткового оновлення сутності).

Вам також потрібно переконатися, що ви використовуєте для цієї операції новий / виділений контекст бази даних сутності.


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

@Charles McIntosh дійсно дав мені відповідь за мою ситуацію в тому, що передане в моделі відсторонено. Для мене те, що в кінцевому підсумку спрацювало, - це збереження переданих у моделі спочатку ... потім продовження додавання дітей, як я вже був раніше:

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.