ti-enxame.com

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

Entity Framework 4, objetos POCO e ASP.Net MVC2. Eu tenho um relacionamento de muitos para muitos, digamos entre BlogPost e entidades de tag. Isso significa que, na minha classe POCO BlogPost gerada por T4, 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 o envio para outra camada (Exibir no aplicativo MVC). Mais tarde, recebo de volta o BlogPost atualizado com propriedades e relacionamentos alterados. Por exemplo, ele tinha as tags "A" "B" e "C", e as novas tags são "C" e "D". No meu exemplo em particular, não há novos Tags e as propriedades dos Tags nunca mudam; portanto, a única coisa que deve ser salva são os relacionamentos alterados. Agora eu 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 o que encontrei:

  • Controller.UpdateModel e Controller.TryUpdateModel não funcionam.
  • Obter o BlogPost antigo do contexto e modificar a coleção não funciona. (com métodos diferentes do próximo ponto)
  • Isso provavelmente funcionaria, mas espero que seja apenas uma solução alternativa, não a solução :(.
  • Tentei anexar/Adicionar/ChangeObjectState funções para BlogPost e/ou Tags em todas as combinações possíveis. Falhou.
  • Isso parece com o que eu preciso, mas não funciona (tentei corrigi-lo, mas não posso).
  • Tentei ChangeState/Add/Attach/... os objetos de relacionamento do contexto. Falhou.

"Não funciona" significa que, na maioria dos casos, trabalhei na "solução" fornecida até que ela não produza erros e salve pelo menos as propriedades do BlogPost. O que acontece com os relacionamentos varia: geralmente as Tags são adicionadas novamente à tabela Tag com novas PKs e o BlogPost salvo faz referência a elas e não às originais. É claro que os Tags retornados possuem PKs e, antes dos métodos de salvar/atualizar, verifico as PKs e elas são iguais às do banco de dados; portanto, a EF provavelmente pensa que são novos objetos e essas PKs são temporárias.

Um problema que eu conheço e pode tornar impossível encontrar uma solução simples e automatizada: Quando a coleção de um objeto POCO é alterada, isso deve ocorrer pela propriedade da coleção virtual mencionada acima, porque o truque FixupCollection atualiza 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 da EF4-POCO-MVC :(. Isso também significa que a EF não pode fazer isso no ambiente MVC, o que for Os tipos de objeto EF4 são usados ​​:(. Acho que o rastreamento de alterações baseado em snapshot deve descobrir que o BlogPost alterado tem relacionamentos com Tags com PKs existentes.

Btw: Eu 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 se isso funcionar, não me ajudará nos meus seis relacionamentos muitos-para-muitos no meu aplicativo :(.

107
peterfoldi

Vamos tentar desta maneira:

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

Editar:

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

Histórico do problema

O EF precisa rastrear alterações nas entidades para que a persistência saiba quais registros devem ser atualizados, inseridos ou excluídos. O problema é que é responsabilidade do ObjectContext rastrear as alterações. O ObjectContext pode rastrear alterações apenas para entidades anexadas. As entidades criadas fora do ObjectContext não são rastreadas.

Descrição do problema

Com base na descrição acima, podemos afirmar claramente que o EF é mais adequado para cenários conectados em que a entidade está sempre anexada ao contexto - típico para o aplicativo WinForm. Os aplicativos da Web requerem 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 ao novo contexto e persistido. A recreação geralmente ocorre fora do escopo do contexto (arquitetura em camadas com persistência ignorada).

Solução

Então, como lidar com esse cenário desconectado? Ao usar as classes POCO, temos três maneiras de lidar com o rastreamento de alterações:

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

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

A sincronização manual é proposta como solução pela documentação do MSDN: Anexando e desanexando objetos 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 desanexado, use um dos seguintes métodos.

Os métodos mencionados são ChangeObjectState e ChangeRelationshipState of ObjectStateManager = rastreamento manual de alterações. 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á postagem no blog relacionada ao EF v1 que critica exatamente esse comportamento do EF.

Motivo da solução

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

Outra solução proposta

Em vez da funcionalidade de mesclagem real, a equipe da EF fornece algo chamado Entidades de rastreamento automático (STE) que não resolve o problema. Antes de tudo, o STE funciona apenas se a mesma instância for usada para todo o processamento. No aplicativo da web, esse não é o caso, a menos que você armazene a instância no estado de exibição ou na sessão. Devido a isso, estou muito infeliz com o EF e vou verificar os recursos do NHibernate. A primeira observação diz que o NHibernate talvez tenha tal funcionalidade .

Conclusão

Terminarei essas suposições com um único link para outro 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 é suportada, acredito nele.

Mas ainda existe a possibilidade de eu estar completamente errado e existir alguma funcionalidade de mesclagem automática no EF.

Editar 2:

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

143
Ladislav Mrnka

Eu 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 um diff do gráfico fornecido e do gráfico persistente.

No momento, usando o Entity Framework, você precisará executar as atualizações manualmente, verificar se cada contato é novo e adicionar, verificar se atualizado e editar, verificar se foi removido e excluí-lo do banco de dados. Depois de fazer isso para alguns agregados diferentes em um sistema grande, 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- de um gráfico de entidades desanexadas /

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

19
brentmckendrick

Sei que é tarde para o OP, mas como esse é um problema muito comum, postei isso no caso de servir a outra pessoa. Venho brincando com esse problema e acho que tenho 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 o objeto atualizado, incluindo as coleções que preciso atualizar.
  3. Consulte e converta .ToList () as entidades que eu quero que minha coleção inclua.
  4. Atualize as coleções do objeto principal para a Lista que recebi da etapa 3.
  5. SaveChanges ();

No exemplo a seguir "dataobj" e "_categories" são os parâmetros recebidos pelo meu controlador "dataobj" é meu objeto principal e "_categories" é um IEnumerable que contém os IDs das categorias que o usuário selecionou na exibiçã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 múltiplas relações

9
c0y0teX

A equipe do Entity Framework está ciente de que esse é um problema de usabilidade e planeja solucioná-lo após o EF6.

Da equipe do Entity Framework:

Esse é um problema de usabilidade que estamos cientes e é algo em que estamos pensando e planejamos fazer mais trabalhos no pós-EF6. Eu 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 - recomendamos que você para votar se você ainda não o fez.

Se isso afetar você, vote no recurso em

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

7
Eric J.

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

Descobri que, se eu não usasse o relacionamento na entidade pai, mas apenas adicionasse e removesse as entidades filho, tudo funcionaria bem.

Desculpe pelo VB mas é nisso que está escrito o projeto em que estou trabalhando).

A entidade pai "Relatório" tem uma relação de um para muitos com "RelatórioRole" e tem a propriedade "RelatórioRoles". As novas funções são transmitidas por uma sequência separada por vírgula de uma chamada Ajax.

A primeira linha removerá todas as entidades filhas e, se eu usasse "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}))
1
Alan Bridges