Jak dodawać / aktualizować jednostki podrzędne podczas aktualizowania jednostki nadrzędnej w EF

151

Te dwie jednostki są relacjami jeden-do-wielu (zbudowane przez kod pierwszy fluent 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; }
}

W moim kontrolerze WebApi mam akcje, aby utworzyć podmiot nadrzędny (który działa dobrze) i zaktualizować podmiot nadrzędny (który ma jakiś problem). Akcja aktualizacji wygląda następująco:

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

Obecnie mam dwa pomysły:

  1. Uzyskaj śledzoną jednostkę nadrzędną o nazwie existingprzez model.Idi przypisz wartości modelpo kolei do encji. To brzmi głupio. I model.Childrennie wiem, które dziecko jest nowe, które jest modyfikowane (a nawet usuwane).

  2. Utwórz nową jednostkę nadrzędną za pośrednictwem modeli dołącz ją do DbContext i zapisz. Ale w jaki sposób DbContext może poznać stan elementów podrzędnych (nowe dodawanie / usuwanie / modyfikowanie)?

Jaki jest prawidłowy sposób zaimplementowania tej funkcji?

Cheng Chen
źródło
Zobacz także przykład z GraphDiff w zduplikowanym pytaniu stackoverflow.com/questions/29351401/…
Michael Freidgeim

Odpowiedzi:

219

Ponieważ model, który jest wysyłany do kontrolera WebApi, jest odłączany od dowolnego kontekstu platformy encji (EF), jedyną opcją jest załadowanie wykresu obiektu (nadrzędnego, w tym jego dzieci) z bazy danych i porównanie, które elementy podrzędne zostały dodane, usunięte lub zaktualizowany. (Chyba że śledziłbyś zmiany własnym mechanizmem śledzenia w stanie odłączonym (w przeglądarce lub gdziekolwiek), który moim zdaniem jest bardziej złożony niż następujący.) Mogłoby to wyglądać tak:

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.SetValuesmoże przyjmować dowolny obiekt i mapuje wartości właściwości do dołączonej jednostki na podstawie nazwy właściwości. Jeśli nazwy właściwości w modelu różnią się od nazw w encji, nie możesz użyć tej metody i musisz przypisać wartości pojedynczo.

Slauma
źródło
35
Ale dlaczego ef nie ma bardziej „genialnego” sposobu? Myślę, że ef może wykryć, czy dziecko jest zmodyfikowane / usunięte / dodane, IMO powyższy kod może być częścią struktury EF i stać się bardziej ogólnym rozwiązaniem.
Cheng Chen
7
@DannyChen: To rzeczywiście długa prośba, aby aktualizacja odłączonych jednostek była obsługiwana przez EF w wygodniejszy sposób ( entityframework.codeplex.com/workitem/864 ), ale nadal nie jest częścią frameworka. Obecnie możesz wypróbować tylko bibliotekę „GraphDiff” innej firmy, o której jest mowa w tym elemencie roboczym codeplex, lub napisać kod ręczny, jak w mojej odpowiedzi powyżej.
Slauma
7
Jedna rzecz do dodania: w ramach foreach aktualizacji i wstawiania elementów potomnych nie można tego zrobić, existingParent.Children.Add(newChild)ponieważ wówczas wyszukiwanie istniejącego linqChild zwróci ostatnio dodaną jednostkę, a więc ta jednostka zostanie zaktualizowana. Wystarczy wstawić do tymczasowej listy, a następnie dodać.
Erre Efe
3
@ RandolfRincónFadul Właśnie natknąłem się na ten problem. Moją poprawką, która jest nieco mniej wymagająca, jest zmiana klauzuli where w existingChildzapytaniu LINQ:.Where(c => c.ID == childModel.ID && c.ID != default(int))
Gavin Ward,
2
@RalphWillgoss Jaka jest poprawka w wersji 2.2, o której mówiłeś?
Jan Paolo Go
11

Bawiłem się czymś takim ...

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

do którego możesz zadzwonić za pomocą czegoś takiego:

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

Niestety, ten rodzaj przewraca się, jeśli istnieją właściwości kolekcji w typie podrzędnym, które również wymagają aktualizacji. Rozważ próbę rozwiązania tego problemu przez przekazanie IRepository (z podstawowymi metodami CRUD), które byłoby odpowiedzialne za samodzielne wywołanie UpdateChildCollection. Wywołałby repozytorium zamiast bezpośrednich wywołań DbContext.Entry.

Nie mam pojęcia, jak to wszystko będzie działać na dużą skalę, ale nie wiem, co jeszcze zrobić z tym problemem.

brettman
źródło
1
Świetne rozwiązanie! Ale kończy się niepowodzeniem, jeśli dodasz więcej niż jeden nowy element, zaktualizowany słownik nie może mieć dwukrotnie identyfikatora zero. Potrzebujesz trochę pracy. A także kończy się niepowodzeniem, jeśli relacja to N -> N, w rzeczywistości element jest dodawany do bazy danych, ale tabela N -> N nie jest modyfikowana.
RenanStr
1
toAdd.ForEach(i => (selector(dbItem) as ICollection<Tchild>).Add(i.Value));powinien rozwiązać problem n -> n.
RenanStr
10

W porzadku chlopaki. Miałem tę odpowiedź raz, ale zgubiłem ją po drodze. absolutna tortura, kiedy wiesz, że jest lepszy sposób, ale nie możesz go sobie przypomnieć ani znaleźć! To jest bardzo proste. Właśnie przetestowałem to na wiele sposobów.

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

Możesz zastąpić całą listę nową! Kod SQL usunie i doda jednostki w razie potrzeby. Nie musisz się tym przejmować. Pamiętaj, aby dołączyć kolekcję podrzędną lub nie mieć kości. Powodzenia!

Charles McIntosh
źródło
Właśnie tego potrzebuję, ponieważ liczba dzieci w moim modelu jest na ogół dość mała, więc zakładając, że Linq początkowo usunie wszystkie oryginalne elementy potomne z tabeli, a następnie doda wszystkie nowe, wpływ na wydajność nie jest problemem.
William T. Mallard
@Charles McIntosh. Nie rozumiem, dlaczego ponownie ustawiasz dzieci, gdy uwzględniasz je w początkowym zapytaniu?
pantonis
1
@pantonis Dołączam kolekcję podrzędną, aby można ją było załadować do edycji. Jeśli polegam na leniwym ładowaniu, aby to zrozumieć, to nie działa. Ustawiłem dzieci (raz), ponieważ zamiast ręcznie usuwać i dodawać elementy do kolekcji, mogę po prostu zastąpić listę, a entityframework będzie dodawał i usuwał elementy za mnie. Kluczem jest ustawienie stanu jednostki na zmodyfikowane i umożliwienie Frameworkowi wykonania ciężkiego podnoszenia.
Charles McIntosh
@CharlesMcIntosh Nadal nie rozumiem, co próbujesz osiągnąć z tamtymi dziećmi. Umieściłeś to w pierwszym żądaniu (Include (p => p.Children). Dlaczego prosisz o to ponownie?
pantonis
@pantonis, musiałem wyciągnąć starą listę za pomocą .include (), aby została załadowana i dołączona jako kolekcja z bazy danych. W ten sposób wywoływane jest leniwe ładowanie. bez niego żadne zmiany na liście nie byłyby śledzone, gdybym użył entitystate.modified. aby powtórzyć, to, co robię, to ustawienie bieżącej kolekcji podrzędnej na inną kolekcję podrzędną. jak gdyby menedżer miał kilku nowych pracowników lub kilku stracił. Użyłbym zapytania, aby uwzględnić lub wykluczyć tych nowych pracowników i po prostu zastąpić starą listę nową listą, a następnie pozwolić EF dodawać lub usuwać w razie potrzeby po stronie bazy danych.
Charles McIntosh,
9

Jeśli używasz EntityFrameworkCore, możesz wykonać następujące czynności w akcji post kontrolera ( metoda Attach rekursywnie dołącza właściwości nawigacji, w tym kolekcje):

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

Zakłada się, że każda zaktualizowana jednostka ma wszystkie właściwości ustawione i podane w danych postu od klienta (np. Nie będzie działać przy częściowej aktualizacji jednostki).

Musisz również upewnić się, że do tej operacji używasz nowego / dedykowanego kontekstu bazy danych platformy jednostki.

hallz
źródło
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));
        }

W ten sposób rozwiązałem ten problem. W ten sposób EF wie, które dodać, które zaktualizować.

Jokeur
źródło
Działał jak urok! Dzięki.
Inktkiller
2

Istnieje kilka projektów, które ułatwiają interakcję między klientem a serwerem, jeśli chodzi o zapisywanie całego grafu obiektu.

Oto dwa, na które chciałbyś spojrzeć:

Oba powyższe projekty rozpoznają odłączone jednostki po ich zwróceniu na serwer, wykrywają i zapisują zmiany oraz zwracają dane klienta.

Shimmy Weitzhandler
źródło
1

Samo potwierdzenie koncepcji Controler.UpdateModel nie będzie działać poprawnie.

Pełna klasa tutaj :

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
źródło
0

@Charles McIntosh naprawdę dał mi odpowiedź na moją sytuację, ponieważ przekazany model został odłączony. Dla mnie to, co ostatecznie zadziałało, to najpierw zapisanie przekazanego modelu ... a następnie kontynuowanie dodawania dzieci, tak jak byłem wcześniej:

public async Task<IHttpActionResult> GetUPSFreight(PartsExpressOrder order)
{
    db.Entry(order).State = EntityState.Modified;
    db.SaveChanges();
  ...
}
Anthony Griggs
źródło
0

Dla programistów VB.NET Użyj tego ogólnego sub do oznaczenia stanu podrzędnego, łatwego w użyciu

Uwagi:

  • PromatCon: obiekt jednostki
  • amList: to lista podrzędna, którą chcesz dodać lub zmodyfikować
  • rList: to lista podrzędna, którą chcesz usunąć
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()
Bazylia
źródło
0
var parent = context.Parent.FirstOrDefault(x => x.Id == modelParent.Id);
if (parent != null)
{
  parent.Childs = modelParent.Childs;
}

źródło

Alex
źródło
0

Oto mój kod, który działa dobrze.

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;
        }
Deweloper
źródło