Come aggiungere / aggiornare entità figlio durante l'aggiornamento di un'entità padre in EF


151

Le due entità sono una relazione uno-a-molti (costruita dal codice api fluente).

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

Nel mio controller WebApi ho delle azioni per creare un'entità padre (che funziona correttamente) e aggiornare un'entità padre (che presenta qualche problema). L'azione di aggiornamento è simile a:

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

Attualmente ho due idee:

  1. Ottieni un'entità padre tracciata denominata existingda model.Ide assegna i valori modeluno a uno all'entità. Sembra stupido. E in model.Childrennon so quale bambino è nuovo, quale bambino viene modificato (o addirittura eliminato).

  2. Creare una nuova entità padre tramite modele collegarla a DbContext e salvarla. Ma come può il DbContext conoscere lo stato dei bambini (nuovo aggiungi / elimina / modificato)?

Qual è il modo corretto di implementare questa funzione?


Vedi anche l'esempio con GraphDiff in una domanda duplicato stackoverflow.com/questions/29351401/...
Michael Freidgeim

Risposte:


220

Poiché il modello che viene pubblicato nel controller WebApi è staccato da qualsiasi contesto di framework di entità (EF), l'unica opzione è quella di caricare il grafico degli oggetti (padre compreso i suoi figli) dal database e confrontare quali figli sono stati aggiunti, eliminati o aggiornato. (A meno che non seguirai le modifiche con il tuo meccanismo di tracciamento durante lo stato distaccato (nel browser o ovunque) che secondo me è più complesso di quanto segue.) Potrebbe apparire così:

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.SetValuespuò prendere qualsiasi oggetto e mappare i valori delle proprietà sull'entità collegata in base al nome della proprietà. Se i nomi delle proprietà nel modello sono diversi dai nomi nell'entità non è possibile utilizzare questo metodo e è necessario assegnare i valori uno per uno.


35
Ma perché ef non ha un modo più "brillante"? Penso che ef possa rilevare se il bambino viene modificato / cancellato / aggiunto, il tuo codice IMO sopra può far parte del framework EF e diventare una soluzione più generica.
Cheng Chen,

7
@DannyChen: in effetti è una lunga richiesta che l'aggiornamento di entità disconnesse sia supportato da EF in un modo più comodo ( entityframework.codeplex.com/workitem/864 ) ma non fa ancora parte del framework. Attualmente puoi solo provare la lib di terze parti "GraphDiff" menzionata in quel codeplex o scrivere codice manuale come nella mia risposta sopra.
Slauma,

7
Una cosa da aggiungere: all'interno di foreach dell'aggiornamento e dell'inserimento di elementi existingParent.Children.Add(newChild)secondari , non è possibile farlo perché la ricerca linqChild esistente restituirà l'entità aggiunta di recente e tale entità verrà aggiornata. Hai solo bisogno di inserire in un elenco temporaneo e quindi aggiungere.
Erre Efe,

3
@ RandolfRincónFadul Mi sono appena imbattuto in questo problema. La mia correzione, che è un po 'meno sforzo, è quella di cambiare la clausola where nella existingChildquery LINQ:.Where(c => c.ID == childModel.ID && c.ID != default(int))
Gavin Ward,

2
@RalphWillgoss Qual è la soluzione di cui stavi parlando in 2.2?
Jan Paolo,

11

Ho fatto un casino con qualcosa del genere ...

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

che puoi chiamare con qualcosa del tipo:

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

Sfortunatamente, questo tipo cade se ci sono proprietà di raccolta sul tipo figlio che devono anche essere aggiornate. Considerare di provare a risolvere questo problema passando un IRepository (con metodi CRUD di base) che sarebbe responsabile di chiamare UpdateChildCollection da solo. Chiamerebbe il repository anziché le chiamate dirette a DbContext.Entry.

Non ho idea di come tutto ciò funzionerà su larga scala, ma non sono sicuro di cos'altro fare con questo problema.


1
Ottima soluzione! Ma non riesce se aggiungi più di un nuovo elemento, il dizionario aggiornato non può avere zero id due volte. Ho bisogno di un po 'di lavoro in giro. E fallisce anche se la relazione è N -> N, infatti l'elemento viene aggiunto al database, ma la tabella N -> N non viene modificata.
RenanStr,

1
toAdd.ForEach(i => (selector(dbItem) as ICollection<Tchild>).Add(i.Value));dovrebbe risolvere il problema n -> n.
RenanStr,

10

Ok ragazzi. Ho avuto questa risposta una volta ma l'ho persa lungo la strada. tortura assoluta quando sai che c'è un modo migliore ma non riesci a ricordarlo o trovarlo! È molto semplice. L'ho appena provato in diversi modi.

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

Puoi sostituire l'intero elenco con uno nuovo! Il codice SQL rimuoverà e aggiungerà entità secondo necessità. Non c'è bisogno di preoccuparsi di quello. Assicurati di includere la raccolta dei bambini o nessun dado. In bocca al lupo!


Proprio quello di cui ho bisogno, poiché il numero di figli nel mio modello è generalmente piuttosto piccolo, quindi supponendo che Linq eliminerà inizialmente tutti i figli originali dalla tabella e quindi aggiungerà tutti quelli nuovi, l'impatto sulle prestazioni non è un problema.
William T. Mallard,

@Charles McIntosh. Non capisco perché imposti nuovamente Children mentre lo includi nella query iniziale?
pantonis,

1
@pantonis Includo la raccolta figlio in modo che possa essere caricata per la modifica. Se faccio affidamento sul caricamento lento per capirlo, non funziona. Ho impostato i figli (una volta) perché, invece di eliminare manualmente e aggiungere elementi alla raccolta, posso semplicemente sostituire l'elenco e Entity Frame dovrebbe aggiungere ed eliminare elementi per me. La chiave sta impostando lo stato dell'entità su modificato e permettendo al lavoro dell'entità di fare il lavoro pesante.
Charles McIntosh,

@CharlesMcIntosh Non capisco ancora cosa stai cercando di ottenere con i bambini lì. L'hai incluso nella prima richiesta (Includi (p => p. Bambini). Perché lo richiedi di nuovo?
pantonis

@pantonis, ho dovuto estrarre la vecchia lista usando .include () in modo che venisse caricata e allegata come una raccolta dal database. È come viene invocato il caricamento lento. senza di essa, qualsiasi modifica all'elenco non verrebbe tracciata quando avessi usato entitystate.modified. per ribadire, quello che sto facendo è impostare la raccolta figlio corrente su una raccolta figlio diversa. come se un manager avesse un sacco di nuovi dipendenti o ne perdesse alcuni. Vorrei utilizzare una query per includere o escludere quei nuovi dipendenti e semplicemente sostituire il vecchio elenco con un nuovo elenco, quindi consentire a EF di aggiungere o eliminare, se necessario, dal lato del database.
Charles McIntosh,

9

Se si utilizza EntityFrameworkCore è possibile effettuare le seguenti operazioni nel post controller (il metodo Allega collega in modo ricorsivo le proprietà di navigazione, comprese le raccolte):

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

Si presume che ogni entità che è stata aggiornata abbia tutte le proprietà impostate e fornite nei dati di post dal client (ad es. Non funzionerà per l'aggiornamento parziale di un'entità).

È inoltre necessario assicurarsi di utilizzare un contesto di database framework entità nuovo / dedicato per questa operazione.


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

Ecco come ho risolto questo problema. In questo modo, EF sa quale aggiungere quale aggiornare.


Ha funzionato come un fascino! Grazie.
Inktkiller

2

Esistono alcuni progetti che rendono più semplice l'interazione tra client e server per quanto riguarda il salvataggio di un intero oggetto grafico.

Eccone due che vorresti guardare:

Entrambi i progetti di cui sopra riconoscono le entità disconnesse quando vengono restituite al server, rilevano e salvano le modifiche e restituiscono i dati interessati dal client.


1

Solo una prova del concetto Controler.UpdateModel non funzionerà correttamente.

Classe completa qui :

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 mi ha davvero dato la risposta per la mia situazione in quanto il modello passato è stato staccato. Per me quello che alla fine ha funzionato è stato salvare prima il modello approvato ... poi continuando ad aggiungere i bambini come ero già prima:

public async Task<IHttpActionResult> GetUPSFreight(PartsExpressOrder order)
{
    db.Entry(order).State = EntityState.Modified;
    db.SaveChanges();
  ...
}

0

Per gli sviluppatori VB.NET Utilizzare questo sub generico per contrassegnare lo stato figlio, facile da usare

Appunti:

  • PromatCon: l'oggetto entità
  • amList: è l'elenco figlio che si desidera aggiungere o modificare
  • rList: è l'elenco figlio che si desidera rimuovere
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;
}

fonte


0

Ecco il mio codice che funziona perfettamente.

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;
        }
Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.