Cómo agregar / actualizar entidades secundarias al actualizar una entidad principal en EF

151

Las dos entidades son una relación de uno a muchos (construido por la primera API fluida del código).

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

En mi controlador WebApi, tengo acciones para crear una entidad principal (que funciona bien) y actualizar una entidad principal (que tiene algún problema). La acción de actualización se ve así:

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

Actualmente tengo dos ideas:

  1. Obtenga una entidad principal rastreada nombrada existingpor model.Idy asigne valores modeluno por uno a la entidad. Esto suena estúpido. Y en model.ChildrenNo sé qué hijo es nuevo, qué hijo se modifica (o incluso se elimina).

  2. Cree una nueva entidad principal a través de model, y adjúntela al DbContext y guárdela. Pero, ¿cómo puede saber DbContext el estado de los hijos (nueva adición / eliminación / modificación)?

¿Cuál es la forma correcta de implementar esta función?

Cheng Chen
fuente
Vea también el ejemplo con GraphDiff en una pregunta duplicada stackoverflow.com/questions/29351401/…
Michael Freidgeim

Respuestas:

219

Debido a que el modelo que se publica en el controlador WebApi se separa de cualquier contexto de entidad-marco (EF), la única opción es cargar el gráfico de objeto (padre, incluidos sus hijos) de la base de datos y comparar qué hijos se han agregado, eliminado o actualizado. (A menos que realice un seguimiento de los cambios con su propio mecanismo de seguimiento durante el estado desconectado (en el navegador o donde sea), que en mi opinión es más complejo que el siguiente). Podría verse así:

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.SetValuespuede tomar cualquier objeto y asignar valores de propiedad a la entidad adjunta en función del nombre de la propiedad. Si los nombres de propiedad en su modelo son diferentes de los nombres en la entidad, no puede usar este método y debe asignar los valores uno por uno.

Slauma
fuente
35
Pero, ¿por qué ef no tiene una forma más "brillante"? Creo que ef puede detectar si el niño se modifica / elimina / agrega, IMO su código anterior puede ser parte del marco EF y convertirse en una solución más genérica.
Cheng Chen el
77
@DannyChen: De hecho, es una solicitud larga que la actualización de entidades desconectadas debe ser respaldada por EF de una manera más cómoda ( entityframework.codeplex.com/workitem/864 ) pero aún no es parte del marco. Actualmente solo puede probar la lib de terceros "GraphDiff" que se menciona en ese elemento de trabajo codeplex o escribir código manual como en mi respuesta anterior.
Slauma
77
Una cosa para agregar: dentro del alcance de la actualización e inserción de elementos existingParent.Children.Add(newChild)secundarios , no se puede hacer porque la búsqueda existente de Linux linq devolverá la entidad recientemente agregada, por lo que esa entidad se actualizará. Solo necesita insertar en una lista temporal y luego agregar.
Erre Efe
3
@ RandolfRincónFadul Acabo de encontrarme con este problema. Mi solución, que requiere un poco menos de esfuerzo, es cambiar la cláusula where en la existingChildconsulta LINQ:.Where(c => c.ID == childModel.ID && c.ID != default(int))
Gavin Ward,
2
@RalphWillgoss ¿Cuál es la solución en 2.2 de la que hablaba?
Jan Paolo Go
11

He estado jugando con algo como esto ...

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

que puedes llamar con algo como:

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

Desafortunadamente, esto se cae si hay propiedades de colección en el tipo secundario que también necesitan actualizarse. Considerar tratar de resolver esto pasando un IRepository (con métodos CRUD básicos) que sería responsable de llamar a UpdateChildCollection por sí solo. Llamaría al repositorio en lugar de llamadas directas a DbContext.Entry.

No tengo idea de cómo funcionará todo a escala, pero no estoy seguro de qué más hacer con este problema.

brettman
fuente
1
Gran solución! Pero falla si agrega más de un elemento nuevo, el diccionario actualizado no puede tener cero identificación dos veces. Necesita algo de trabajo cerca. Y también falla si la relación es N -> N, de hecho, el elemento se agrega a la base de datos, pero la tabla N -> N no se modifica.
RenanStr
1
toAdd.ForEach(i => (selector(dbItem) as ICollection<Tchild>).Add(i.Value));debería resolver el problema n -> n.
RenanStr
10

Ok muchachos. Tuve esta respuesta una vez, pero la perdí en el camino. ¡Tortura absoluta cuando sabes que hay una mejor manera, pero no puedo recordarla ni encontrarla! Es muy simple. Acabo de probarlo de varias maneras.

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

¡Puede reemplazar toda la lista por una nueva! El código SQL eliminará y agregará entidades según sea necesario. No hay necesidad de preocuparse por eso. Asegúrese de incluir la colección infantil o ningún dado. ¡Buena suerte!

Charles McIntosh
fuente
Justo lo que necesito, ya que la cantidad de niños en mi modelo es generalmente bastante pequeña, por lo que suponiendo que Linq eliminará todos los niños originales de la tabla inicialmente y luego agregará todos los nuevos, el impacto en el rendimiento no es un problema.
William T. Mallard
@Charles McIntosh. No entiendo por qué vuelve a configurar Children mientras lo incluye en la consulta inicial.
pantonis
1
@pantonis Incluyo la colección secundaria para que se pueda cargar para editar. Si confío en la carga diferida para resolverlo, no funciona. Configuré los elementos secundarios (una vez) porque en lugar de eliminar y agregar elementos manualmente a la colección, simplemente puedo reemplazar la lista y el marco de la entidad agregará y eliminará elementos para mí. La clave está en modificar el estado de la entidad y permitir que el marco de la entidad haga el trabajo pesado.
Charles McIntosh
@CharlesMcIntosh Todavía no entiendo lo que estás tratando de lograr con los niños allí. Lo incluyó en la primera solicitud (Incluir (p => p.Niños). ¿Por qué lo solicita nuevamente?
pantonis
@pantonis, tuve que extraer la lista anterior usando .include () para que se cargue y se adjunte como una colección de la base de datos. Es cómo se invoca la carga perezosa. sin él, no se realizaría un seguimiento de los cambios en la lista cuando use entitystate.modified. para reiterar, lo que estoy haciendo es configurar la colección secundaria actual en una colección secundaria diferente. como si un gerente tuviera un montón de nuevos empleados o perdiera algunos. Usaría una consulta para incluir o excluir a esos nuevos empleados y simplemente reemplazar la lista anterior con una nueva lista y luego dejar que EF agregue o elimine según sea necesario desde el lado de la base de datos.
Charles McIntosh
9

Si está utilizando EntityFrameworkCore, puede hacer lo siguiente en la acción posterior de su controlador (El método Adjuntar adjunta recursivamente propiedades de navegación, incluidas colecciones):

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

Se supone que cada entidad que se actualizó tiene todas las propiedades establecidas y proporcionadas en los datos de publicación del cliente (por ejemplo, no funcionará para la actualización parcial de una entidad).

También debe asegurarse de estar utilizando un contexto de base de datos de marco de entidad nuevo / dedicado para esta operación.

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

Así es como resolví este problema. De esta manera, EF sabe qué agregar y qué actualizar.

Broma
fuente
¡Trabajado como un encanto! Gracias.
Inktkiller
2

Existen algunos proyectos que facilitan la interacción entre el cliente y el servidor en lo que respecta a guardar un gráfico de objeto completo.

Aquí hay dos que te gustaría ver:

Los dos proyectos anteriores reconocen las entidades desconectadas cuando se devuelve al servidor, detectan y guardan los cambios y devuelven los datos afectados por el cliente.

Shimmy Weitzhandler
fuente
1

La prueba de concepto Controler.UpdateModel no funcionará correctamente.

Clase completa aquí :

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
fuente
0

@Charles McIntosh realmente me dio la respuesta para mi situación en que el modelo aprobado fue separado. Para mí, lo que finalmente funcionó fue guardar primero el modelo aprobado ... luego continuar agregando los niños como ya lo estaba antes:

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

Para desarrolladores de VB.NET Use este sub genérico para marcar el estado secundario, fácil de usar

Notas:

  • PromatCon: el objeto de entidad
  • amList: es la lista secundaria que desea agregar o modificar
  • rList: es la lista secundaria que desea eliminar
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()
Albahaca
fuente
0
var parent = context.Parent.FirstOrDefault(x => x.Id == modelParent.Id);
if (parent != null)
{
  parent.Childs = modelParent.Childs;
}

fuente

Alex
fuente
0

Aquí está mi código que funciona bien.

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