Atualizar relacionamentos ao salvar alterações de objetos EF4 POCO

107

Entity Framework 4, objetos POCO e ASP.Net MVC2. Eu tenho um relacionamento muitos para muitos, digamos, entre entidades BlogPost e Tag. Isso significa que em minha classe POCO BlogPost gerada por T4 eu tenho:

public virtual ICollection<Tag> Tags {
    // getter and setter with the magic FixupCollection
}
private ICollection<Tag> _tags;

Peço um BlogPost e as tags relacionadas de uma instância do ObjectContext e envio para outra camada (Exibir no aplicativo MVC). Mais tarde, recebo de volta o BlogPost atualizado com propriedades alteradas e relacionamentos alterados. Por exemplo, ele tinha tags "A" "B" e "C", e as novas tags são "C" e "D". Em meu exemplo particular, não há novos tags e as propriedades dos tags nunca mudam, então a única coisa que deve ser salva são os relacionamentos alterados. Agora preciso salvar isso em outro ObjectContext. (Atualização: agora tentei fazer na mesma instância de contexto e também falhei.)

O problema: não consigo salvar os relacionamentos corretamente. Eu tentei tudo que encontrei:

  • Controller.UpdateModel e Controller.ExperimenteUpdateModel não funcionam.
  • Obter o antigo BlogPost do contexto e depois modificar a coleção não funciona. (com métodos diferentes a partir do próximo ponto)
  • Isso provavelmente iria funcionar, mas espero que esta é apenas uma solução alternativa, não a solução :(.
  • Experimentei as funções Attach / Add / ChangeObjectState para BlogPost e / ou Tags em todas as combinações possíveis. Falhou.
  • Este parece com o que eu preciso, mas ele não funciona (Tentei corrigi-lo, mas não pode para o meu problema).
  • Tentei ChangeState / Add / Attach / ... os objetos de relacionamento do contexto. Falhou.

"Não funciona" significa, na maioria dos casos, que trabalhei na "solução" fornecida até que ela não produzisse erros e salve pelo menos as propriedades do BlogPost. O que acontece com os relacionamentos varia: geralmente as tags são adicionadas novamente à tabela de tags com novos PKs e o BlogPost salvo faz referência a esses e não aos originais. É claro que os Tags retornados têm PKs, e antes dos métodos salvar / atualizar eu verifico os PKs e eles são iguais aos do banco de dados, então provavelmente EF pensa que eles são objetos novos e aqueles PKs são temporários.

Um problema que conheço e que pode tornar impossível encontrar uma solução automatizada simples: Quando a coleção de um objeto POCO é alterada, isso deve acontecer pela propriedade de coleção virtual mencionada acima, porque então o truque FixupCollection atualizará as referências reversas na outra extremidade do relacionamento muitos para muitos. No entanto, quando uma Visualização "retorna" um objeto BlogPost atualizado, isso não aconteceu. Isso significa que talvez não haja uma solução simples para o meu problema, mas isso me deixaria muito triste e eu odiaria o triunfo EF4-POCO-MVC :(. Isso também significaria que EF não pode fazer isso no ambiente MVC, seja qual for Tipos de objeto EF4 são usados ​​:(. Acho que o rastreamento de alterações baseado em instantâneo deve descobrir que o BlogPost alterado tem relacionamentos com Tags com PKs existentes.

Aliás: acho que o mesmo problema acontece com as relações um-para-muitos (o Google e meu colega dizem isso). Vou tentar em casa, mas mesmo que funcione, isso não me ajuda em meus seis relacionamentos muitos-para-muitos em meu aplicativo :(.

Peterfoldi
fonte
Por favor, poste seu código. Este é um cenário comum.
John Farrell,
1
Eu tenho uma solução automática para este problema, está oculto nas respostas abaixo, por isso muitos sentiriam falta, mas por favor, dê uma olhada porque isso vai lhe poupar um trabalho e tanto, veja a postagem aqui
brentmckendrick
@brentmckendrick Acho que outra abordagem é melhor. Em vez de enviar todo o gráfico do objeto modificado pela rede, por que não enviar apenas o delta? Você não precisaria nem mesmo de classes DTO geradas nesse caso. Se você tiver uma opinião sobre isso de qualquer maneira, vamos discutir em stackoverflow.com/questions/1344066/calculate-object-delta .
HappyNomad

Respostas:

145

Vamos tentar desta forma:

  • Anexe o BlogPost ao contexto. Depois de anexar o objeto ao contexto do estado do objeto, todos os objetos relacionados e todas as relações são definidas como Inalterado.
  • Use context.ObjectStateManager.ChangeObjectState para definir seu BlogPost como Modificado
  • Iterar por meio da coleção de tags
  • Use context.ObjectStateManager.ChangeRelationshipState para definir o estado da relação entre o Tag atual e o BlogPost.
  • SaveChanges

Editar:

Acho que um de meus comentários deu a você uma falsa esperança de que a EF fará a fusão para você. Brinquei muito com esse problema e minha conclusão diz que a EF não fará isso por você. Acho que você também encontrou minha pergunta no MSDN . Na realidade, há muitas perguntas desse tipo na Internet. O problema é que não está claro como lidar com esse cenário. Então, vamos dar uma olhada no problema:

Histórico do problema

EF precisa rastrear mudanças em entidades para que a persistência saiba quais registros devem ser atualizados, inseridos ou deletados. O problema é que é responsabilidade do ObjectContext rastrear as mudanças. ObjectContext é capaz de rastrear alterações apenas para entidades anexadas. As entidades criadas fora do ObjectContext não são rastreadas de forma alguma.

Descrição do Problema

Com base na descrição acima, podemos afirmar claramente que o EF é mais adequado para cenários conectados onde a entidade está sempre anexada ao contexto - típico para aplicativos WinForm. Os aplicativos da Web requerem um cenário desconectado em que o contexto é fechado após o processamento da solicitação e o conteúdo da entidade é passado como resposta HTTP ao cliente. A próxima solicitação HTTP fornece conteúdo modificado da entidade que deve ser recriado, anexado a um novo contexto e persistido. A recreação geralmente acontece fora do escopo do contexto (arquitetura em camadas com ignorância de persistência).

Solução

Então, como lidar com esse cenário desconectado? Ao usar classes POCO, temos 3 maneiras de lidar com o controle de alterações:

  • Instantâneo - requer o mesmo contexto = inútil para cenário desconectado
  • Proxies de rastreamento dinâmico - requer o mesmo contexto = inútil para cenário desconectado
  • Sincronização manual.

A sincronização manual em uma única entidade é uma tarefa fácil. Você só precisa anexar a entidade e chamar AddObject para inserir, DeleteObject para excluir ou definir o estado em ObjectStateManager como Modificado para atualização. A verdadeira dor surge quando você tem que lidar com o gráfico do objeto em vez de uma entidade única. Essa dor é ainda pior quando você tem que lidar com associações independentes (aquelas que não usam propriedade de chave estrangeira) e muitos para muitos relacionamentos. Nesse caso, você deve sincronizar manualmente cada entidade no gráfico de objeto, mas também cada relação no gráfico de objeto.

A sincronização manual é proposta como solução pela documentação do MSDN: Attaching and Detaching objects diz:

Os objetos são anexados ao contexto do objeto em um estado Inalterado. Se você precisar alterar o estado de um objeto ou o relacionamento porque sabe que seu objeto foi modificado no estado separado, use um dos seguintes métodos.

Os métodos mencionados são ChangeObjectState e ChangeRelationshipState de ObjectStateManager = controle de alteração manual. Proposta semelhante está em outro artigo de documentação do MSDN: Definindo e gerenciando relacionamentos diz:

Se você estiver trabalhando com objetos desconectados, deverá gerenciar manualmente a sincronização.

Além disso, há um post no blog relacionado ao EF v1 que critica exatamente esse comportamento do EF.

Razão da solução

EF tem muitas operações e configurações "úteis", como Refresh , Load , ApplyCurrentValues , ApplyOriginalValues , MergeOption etc. Mas, pela minha investigação, todos esses recursos funcionam apenas para uma entidade e afetam apenas propriedades escalares (= não propriedades e relações de navegação). Prefiro não testar esses métodos com tipos complexos aninhados em entidade.

Outra solução proposta

Em vez da funcionalidade de fusão real, a equipe EF fornece algo chamado Self Tracking Entities (STE) que não resolve o problema. Em primeiro lugar, o STE funciona apenas se a mesma instância for usada para todo o processamento. No aplicativo da web, não é o caso, a menos que você armazene a instância no estado de exibição ou sessão. Por isso estou muito insatisfeito com o uso do EF e vou verificar as funcionalidades do NHibernate. A primeira observação diz que o NHibernate talvez tenha essa funcionalidade .

Conclusão

Vou terminar essas suposições com um único link para outra questão relacionada no fórum do MSDN. Verifique a resposta de Zeeshan Hirani. Ele é autor de Entity Framework 4.0 Recipes . Se ele disser que a mesclagem automática de gráficos de objetos não é compatível, eu acredito nele.

Mas ainda existe a possibilidade de que eu esteja completamente errado e alguma funcionalidade de mesclagem automática exista no EF.

Editar 2:

Como você pode ver, isso já foi adicionado ao MS Connect como sugestão em 2007. A MS fechou como algo a ser feito na próxima versão, mas na verdade nada foi feito para melhorar essa lacuna, exceto STE.

Ladislav Mrnka
fonte
7
Esta é uma das melhores respostas que li no SO. Você declarou claramente o que tantos artigos do MSDN, documentação e postagens de blog sobre o tópico não conseguiram transmitir. O EF4 não suporta inerentemente a atualização de relacionamentos de entidades "separadas". Ele apenas fornece ferramentas para você mesmo implementá-lo. Obrigado!
tirano de
1
Então, alguns meses depois, que tal o NHibernate relacionado a esse problema em comparação com o EF4?
CallMeLaNN
1
Isso é muito bem suportado no NHibernate :-) sem necessidade de mesclar manualmente, no meu exemplo é um gráfico de objeto de 3 níveis de profundidade, a pergunta tem respostas, cada resposta tem comentários e a pergunta tem comentários também. NHibernate pode persistir / mesclar seu gráfico de objeto, não importa quão complexo seja ienablemuch.com/2011/01/nhibernate-saves-your-whole-object.html Outro usuário NHibernate satisfeito: codinginstinct.com/2009/11/…
Michael Buen
2
Uma das melhores explicações que já li !! Muito obrigado
marvelTracker
2
A equipe da EF planeja resolver este pós-EF6. Talvez você queira votar em entityframework.codeplex.com/workitem/864
Eric J.
19

Tenho uma solução para o problema descrito acima por Ladislav. Eu criei um método de extensão para o DbContext que executará automaticamente a adição / atualização / exclusão com base em uma comparação do gráfico fornecido e gráfico persistente.

No momento usando o Entity Framework você precisará realizar as atualizações dos contatos manualmente, verificar se cada contato é novo e adicionar, verificar se atualizado e editar, verificar se foi removido e depois excluí-lo do banco de dados. Depois de fazer isso para alguns agregados diferentes em um grande sistema, você começa a perceber que deve haver uma maneira melhor e mais genérica.

Dê uma olhada e veja se isso pode ajudar http://refactorthis.wordpress.com/2012/12/11/introducing-graphdiff-for-entity-framework-code-first-allowing-automated-updates-of-a- gráfico de entidades desanexadas /

Você pode ir direto para o código aqui https://github.com/refactorthis/GraphDiff

brentmckendrick
fonte
Tenho certeza de que você pode resolver essa questão facilmente, estou tendo um péssimo momento com isso.
Shimmy Weitzhandler
1
Oi Shimmy, desculpe, finalmente consegui algum tempo para dar uma olhada. Vou dar uma olhada nisso esta noite.
brentmckendrick
Esta biblioteca é ótima e me economizou muito tempo! THX!
Lordjeb
9

Eu sei que é tarde para o OP, mas como esse é um problema muito comum, postei isso caso seja para outra pessoa. Tenho brincado com esse problema e acho que encontrei uma solução bastante simples, o que faço é:

  1. Salve o objeto principal (Blogs, por exemplo) definindo seu estado como Modificado.
  2. Consulte o banco de dados para obter o objeto atualizado, incluindo as coleções que preciso atualizar.
  3. Consulte e converta .ToList () as entidades que desejo que minha coleção inclua.
  4. Atualize a (s) coleção (ões) do objeto principal para a Lista que obtive na etapa 3.
  5. SaveChanges ();

No exemplo a seguir, "dataobj" e "_categories" são os parâmetros recebidos por meu controlador "dataobj" é meu objeto principal e "_categories" é um IEnumerable contendo os IDs das categorias que o usuário selecionou na visualização.

    db.Entry(dataobj).State = EntityState.Modified;
    db.SaveChanges();
    dataobj = db.ServiceTypes.Include(x => x.Categories).Single(x => x.Id == dataobj.Id);
    var it = _categories != null ? db.Categories.Where(x => _categories.Contains(x.Id)).ToList() : null;
    dataobj.Categories = it;
    db.SaveChanges();

Até funciona para relações múltiplas

c0y0teX
fonte
7

A equipe do Entity Framework está ciente de que este é um problema de usabilidade e planeja resolvê-lo pós-EF6.

Da equipe do Entity Framework:

Este é um problema de usabilidade do qual estamos cientes e é algo em que estivemos pensando e planejamos trabalhar mais no pós-EF6. Criei este item de trabalho para rastrear o problema: http://entityframework.codeplex.com/workitem/864 O item de trabalho também contém um link para o item de voz do usuário para isso - encorajo você a votar nele, se você tiver ainda não o fez.

Se isso afeta você, vote no recurso em

http://entityframework.codeplex.com/workitem/864

Eric J.
fonte
pós-EF6? em que ano será então no caso otimista?
quetzalcoatl
@quetzalcoatl: Pelo menos está no radar deles :-) A EF percorreu um longo caminho desde a EF 1, mas ainda tem um longo caminho a percorrer.
Eric J.
1

Todas as respostas foram ótimas para explicar o problema, mas nenhuma delas realmente resolveu o problema para mim.

Descobri que, se não usar o relacionamento na entidade pai, mas apenas adicionar e remover as entidades filho, tudo funcionará bem.

Desculpe pelo VB, mas é nele que o projeto em que estou trabalhando está escrito.

A entidade pai "Report" tem um relacionamento um para muitos com "ReportRole" e tem a propriedade "ReportRoles". As novas funções são passadas por uma string separada por vírgulas de uma chamada Ajax.

A primeira linha removerá todas as entidades filhas e, se eu usar "report.ReportRoles.Remove (f)" em vez de "db.ReportRoles.Remove (f)", obteria o erro.

report.ReportRoles.ToList.ForEach(Function(f) db.ReportRoles.Remove(f))
Dim newRoles = If(String.IsNullOrEmpty(model.RolesString), New String() {}, model.RolesString.Split(","))
newRoles.ToList.ForEach(Function(f) db.ReportRoles.Add(New ReportRole With {.ReportId = report.Id, .AspNetRoleId = f}))
Alan Bridges
fonte