Como adicionar / atualizar entidades filhas ao atualizar uma entidade pai no EF

151

As duas entidades são um-para-muitos relacionamento (construído pelo código primeiro 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; }
}

No meu controlador WebApi, tenho ações para criar uma entidade pai (que está funcionando bem) e atualizar uma entidade pai (que tem algum problema). A ação de atualização é semelhante a:

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

Atualmente, tenho duas idéias:

  1. Obtenha uma entidade pai rastreada nomeada existingpor model.Ide atribua valores modelum a um à entidade. Isso parece estúpido. E model.Childrennão sei qual filho é novo, qual filho é modificado (ou mesmo excluído).

  2. Crie uma nova entidade pai via modele anexe-a ao DbContext e salve-a. Mas como o DbContext pode conhecer o estado dos filhos (nova adição / exclusão / modificação)?

Qual é a maneira correta de implementar esse recurso?

Cheng Chen
fonte
Veja também exemplo com o GraphDiff em uma pergunta duplicada stackoverflow.com/questions/29351401/…
Michael Freidgeim 4/17/17

Respostas:

219

Como o modelo que é lançado no controlador WebApi é desanexado de qualquer contexto de entidade-estrutura (EF), a única opção é carregar o gráfico de objeto (pai, incluindo seus filhos) do banco de dados e comparar quais filhos foram adicionados, excluídos ou Atualizada. (A menos que você rastreie as alterações com seu próprio mecanismo de rastreamento durante o estado desanexado (no navegador ou em qualquer outro local) que, na minha opinião, seja mais complexo do que o seguinte).

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.SetValuespode levar qualquer objeto e mapear valores de propriedade para a entidade anexada com base no nome da propriedade. Se os nomes de propriedades em seu modelo forem diferentes dos nomes na entidade, você não poderá usar este método e deverá atribuir os valores um a um.

Slauma
fonte
35
Mas por que ef não tem uma maneira mais "brilhante"? Eu acho que ef pode detectar se a criança é modificada / excluída / adicionada. IMO, seu código acima pode fazer parte da estrutura da EF e se tornar uma solução mais genérica.
Cheng Chen
7
@ DannyChen: De fato, é um longo pedido que a atualização de entidades desconectadas seja suportada pela EF de uma maneira mais confortável ( entityframework.codeplex.com/workitem/864 ), mas ainda não faz parte da estrutura. Atualmente, você pode apenas experimentar a lib de terceiros "GraphDiff" mencionada no item de trabalho codeplex ou escrever um código manual como na minha resposta acima.
Slauma
7
Uma coisa a acrescentar: no processo de atualização e inserção de filhos, você não pode fazer isso existingParent.Children.Add(newChild)porque a pesquisa linq existente do Child retornará a entidade adicionada recentemente e, portanto, a entidade será atualizada. Você só precisa inserir em uma lista temporária e adicionar.
precisa saber é o seguinte
3
@ RandolfRincónFadul Acabei de encontrar esta questão. Meu reparo que é um pouco menos esforço é mudar a cláusula onde, em existingChildconsulta LINQ:.Where(c => c.ID == childModel.ID && c.ID != default(int))
Gavin Ward
2
@RalphWillgoss Qual foi o problema no 2.2 que você estava falando?
Jan Paolo Go
11

Eu tenho mexido com algo assim ...

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 você pode chamar com algo como:

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

Infelizmente, isso meio que cai se houver propriedades de coleção no tipo filho que também precisam ser atualizadas. Considere tentar resolver isso passando um IRepository (com métodos básicos de CRUD) que seria responsável por chamar UpdateChildCollection por conta própria. Chamaria o repo em vez de chamadas diretas para DbContext.Entry.

Não tenho idéia de como tudo isso funcionará em escala, mas não tenho certeza do que mais fazer com esse problema.

brettman
fonte
1
Ótima solução! Mas falha se adicionar mais de um novo item, o dicionário atualizado não pode ter zero id duas vezes. Precisa de algum trabalho ao redor. E também falha se o relacionamento for N -> N, na verdade, o item é adicionado ao banco de dados, mas a tabela N -> N não é modificada.
usar o seguinte comando
1
toAdd.ForEach(i => (selector(dbItem) as ICollection<Tchild>).Add(i.Value));deve resolver o problema n -> n.
precisa saber é o seguinte
10

Ok pessoal. Eu tive essa resposta uma vez, mas a perdi no caminho. tortura absoluta quando você sabe que há uma maneira melhor, mas não consegue se lembrar ou encontrar! É muito simples. Eu apenas testei de várias maneiras.

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

Você pode substituir a lista inteira por uma nova! O código SQL removerá e adicionará entidades conforme necessário. Não há necessidade de se preocupar com isso. Certifique-se de incluir a coleção infantil ou nenhum dado. Boa sorte!

Charles McIntosh
fonte
Exatamente o que eu preciso, como o número de filhos no meu modelo geralmente é muito pequeno, assumindo que o Linq exclua todos os filhos originais da tabela inicialmente e adicione todos os novos que o impacto no desempenho não é um problema.
William T. Mallard
@Charles McIntosh. Não entendo por que você define Filhos novamente enquanto o inclui na consulta inicial?
pantonis 23/07/19
1
@ pantonis Eu incluo a coleção filho para que possa ser carregada para edição. Se eu contar com o carregamento lento para descobrir isso, não funcionará. Defino os filhos (uma vez) porque, em vez de excluir e adicionar itens manualmente à coleção, posso simplesmente substituir a lista e o framework de entidade adiciona e exclui itens para mim. A chave é definir o estado da entidade como modificado e permitir que a estrutura da entidade faça o trabalho pesado.
Charles McIntosh
@CharlesMcIntosh Eu ainda não entendo o que você está tentando alcançar com as crianças de lá. Você incluiu no primeiro pedido (Incluir (p => p.Children) Por que você solicitá-lo novamente.?
pantonis
@ pantonis, eu tive que puxar a lista antiga usando .include () para que ela seja carregada e anexada como uma coleção do banco de dados. É como o carregamento lento é chamado. sem ele, nenhuma alteração na lista seria rastreada quando eu usasse entitystate.modified. para reiterar, o que estou fazendo é definir a coleção filho atual para uma coleção filho diferente. como se um gerente tivesse um monte de novos funcionários ou perdesse alguns. Eu usaria uma consulta para incluir ou excluir esses novos funcionários e simplesmente substituir a lista antiga por uma nova lista e deixar a EF adicionar ou excluir, conforme necessário, do lado do banco de dados.
Charles McIntosh
9

Se você estiver usando EntityFrameworkCore, poderá fazer o seguinte na ação pós-controlador (o método Attach anexa recursivamente as propriedades de navegação, incluindo coleções):

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

Supõe-se que cada entidade que foi atualizada possui todas as propriedades definidas e fornecidas nos dados de postagem do cliente (por exemplo, não funcionará para atualização parcial de uma entidade).

Você também precisa certificar-se de estar usando um contexto de banco de dados de estrutura de entidade novo / dedicado para esta operação.

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

Foi assim que resolvi esse problema. Dessa forma, a EF sabe qual adicionar e quais atualizar.

Jokeur
fonte
Trabalhou como um encanto! Obrigado.
Inktkiller 15/04
2

Existem alguns projetos por aí que facilitam a interação entre o cliente e o servidor no que diz respeito a salvar um gráfico inteiro de objetos.

Aqui estão dois que você gostaria de ver:

Os dois projetos acima reconhecem as entidades desconectadas quando retornadas ao servidor, detectam e salvam as alterações e retornam aos dados afetados pelo cliente.

Shimmy Weitzhandler
fonte
1

Apenas a prova de conceito Controler.UpdateModel não funcionará corretamente.

Classe completa aqui :

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

@ Charles McIntosh realmente me deu a resposta para minha situação, pois o modelo passado foi desanexado. Para mim, o que funcionou foi salvar primeiro o modelo passado ... depois continuar adicionando os filhos como eu já era antes:

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

Para desenvolvedores do VB.NET Use essa sub genérica para marcar o estado filho, fácil de usar

Notas:

  • PromatCon: o objeto da entidade
  • amList: é a lista filho que você deseja adicionar ou modificar
  • rList: é a lista filho que você deseja remover
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()
Manjericão
fonte
0
var parent = context.Parent.FirstOrDefault(x => x.Id == modelParent.Id);
if (parent != null)
{
  parent.Childs = modelParent.Childs;
}

fonte

Alex
fonte
0

Aqui está o meu código que funciona muito bem.

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