Actualizar relaciones al guardar cambios de objetos EF4 POCO

107

Entity Framework 4, objetos POCO y ASP.Net MVC2. Tengo una relación de muchos a muchos, digamos entre las entidades BlogPost y Tag. Esto significa que en mi clase BlogPost de POCO generada por T4 tengo:

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

Solicito un BlogPost y las etiquetas relacionadas de una instancia de ObjectContext y lo envío a otra capa (Ver en la aplicación MVC). Más tarde recupero el BlogPost actualizado con propiedades cambiadas y relaciones cambiadas. Por ejemplo, tenía etiquetas "A", "B" y "C", y las nuevas etiquetas son "C" y "D". En mi ejemplo particular, no hay etiquetas nuevas y las propiedades de las etiquetas nunca cambian, por lo que lo único que se debe guardar son las relaciones cambiadas. Ahora necesito guardar esto en otro ObjectContext. (Actualización: ahora intenté hacerlo en la misma instancia de contexto y también fallé).

El problema: no puedo hacer que salve las relaciones correctamente. Probé todo lo que encontré:

  • Controller.UpdateModel y Controller.TryUpdateModel no funcionan.
  • Obtener el BlogPost antiguo del contexto y luego modificar la colección no funciona. (con diferentes métodos del siguiente punto)
  • Esto probablemente funcionaría, pero espero que esto sea solo una solución alternativa, no la solución :(.
  • Probé las funciones Adjuntar / Agregar / CambiarObjetoState para BlogPost y / o Etiquetas en todas las combinaciones posibles. Ha fallado.
  • Esto parece lo que necesito, pero no funciona (traté de arreglarlo, pero no puedo por mi problema).
  • Intenté ChangeState / Add / Attach / ... los objetos de relación del contexto. Ha fallado.

"No funciona" significa en la mayoría de los casos que trabajé en la "solución" dada hasta que no produce errores y guarda al menos las propiedades de BlogPost. Lo que sucede con las relaciones varía: por lo general, las etiquetas se agregan nuevamente a la tabla de etiquetas con nuevas PK y el BlogPost guardado hace referencia a esas y no a las originales. Por supuesto, las etiquetas devueltas tienen PK, y antes de los métodos de guardar / actualizar, verifico las PK y son iguales a las de la base de datos, por lo que probablemente EF piense que son objetos nuevos y esas PK son las temporales.

Un problema que conozco y podría hacer imposible encontrar una solución simple automatizada: cuando se cambia la colección de un objeto POCO, eso debería suceder mediante la propiedad de colección virtual mencionada anteriormente, porque entonces el truco FixupCollection actualizará las referencias inversas en el otro extremo de la relación de muchos a muchos. Sin embargo, cuando una vista "devuelve" un objeto BlogPost actualizado, eso no sucedió. Esto significa que tal vez no haya una solución simple para mi problema, pero eso me entristecería mucho y odiaría el triunfo de EF4-POCO-MVC :(. También eso significaría que EF no puede hacer esto en el entorno MVC cualquiera que sea Se utilizan tipos de objetos EF4 :(. Creo que el seguimiento de cambios basado en instantáneas debería descubrir que el BlogPost modificado tiene relaciones con las etiquetas con los PK existentes.

Por cierto: creo que ocurre el mismo problema con las relaciones uno a muchos (Google y mi colega lo dicen). Lo intentaré en casa, pero incluso si funciona, eso no me ayuda en mis seis relaciones de muchos a muchos en mi aplicación :(.

peterfoldi
fuente
Publique su código. Este es un escenario común.
John Farrell
1
Tengo una solución automática para este problema, está oculta en las respuestas a continuación, por lo que muchos la perderían, pero por favor, eche un vistazo, ya que le ahorrará un gran trabajo, consulte la publicación aquí
brentmckendrick
@brentmckendrick Creo que otro enfoque es mejor. En lugar de enviar todo el gráfico de objetos modificado por el cable, ¿por qué no enviar simplemente el delta? En ese caso, ni siquiera necesitaría clases DTO generadas. Si tiene una opinión sobre esto de cualquier manera, hablemos de esto en stackoverflow.com/questions/1344066/calculate-object-delta .
HappyNomad

Respuestas:

145

Intentémoslo de esta manera:

  • Adjunte BlogPost al contexto. Después de adjuntar el objeto al contexto, el estado del objeto, todos los objetos relacionados y todas las relaciones se establecen en Sin cambios.
  • Utilice context.ObjectStateManager.ChangeObjectState para configurar su BlogPost en Modificado
  • Iterar a través de la colección de etiquetas
  • Utilice context.ObjectStateManager.ChangeRelationshipState para establecer el estado de la relación entre la etiqueta actual y BlogPost.
  • Guardar cambios

Editar:

Supongo que uno de mis comentarios te dio falsas esperanzas de que EF haría la fusión por ti. Jugué mucho con este problema y mi conclusión dice que EF no hará esto por ti. Creo que también ha encontrado mi pregunta en MSDN . En realidad, hay muchas preguntas de este tipo en Internet. El problema es que no se indica claramente cómo afrontar este escenario. Así que echemos un vistazo al problema:

Antecedentes del problema

EF necesita rastrear los cambios en las entidades para que la persistencia sepa qué registros deben actualizarse, insertarse o eliminarse. El problema es que es responsabilidad de ObjectContext realizar un seguimiento de los cambios. ObjectContext puede rastrear cambios solo para entidades adjuntas. Las entidades que se crean fuera de ObjectContext no se rastrean en absoluto.

Descripción del problema

Según la descripción anterior, podemos afirmar claramente que EF es más adecuado para escenarios conectados donde la entidad siempre está adjunta al contexto, típico de la aplicación WinForm. Las aplicaciones web requieren un escenario desconectado donde el contexto se cierra después del procesamiento de la solicitud y el contenido de la entidad se pasa como respuesta HTTP al cliente. La siguiente solicitud HTTP proporciona contenido modificado de la entidad que debe recrearse, adjuntarse al nuevo contexto y conservarse. La recreación generalmente ocurre fuera del alcance del contexto (arquitectura en capas con ignorancia persistente).

Solución

Entonces, ¿cómo lidiar con un escenario tan desconectado? Cuando usamos clases de POCO, tenemos 3 formas de lidiar con el seguimiento de cambios:

  • Instantánea: requiere el mismo contexto = inútil para un escenario desconectado
  • Proxies de seguimiento dinámico: requiere el mismo contexto = inútil para un escenario desconectado
  • Sincronización manual.

La sincronización manual en una sola entidad es una tarea sencilla. Solo necesita adjuntar la entidad y llamar a AddObject para insertar, DeleteObject para eliminar o establecer el estado en ObjectStateManager en Modified para actualizar. El verdadero dolor surge cuando tienes que lidiar con un gráfico de objetos en lugar de una entidad única. Este dolor es aún peor cuando tiene que lidiar con asociaciones independientes (aquellas que no usan propiedad de clave externa) y muchas a muchas relaciones. En ese caso, debe sincronizar manualmente cada entidad en el gráfico de objetos, pero también cada relación en el gráfico de objetos.

La documentación de MSDN propone la sincronización manual como solución: Adjuntar y desconectar objetos dice:

Los objetos se adjuntan al contexto del objeto en un estado Sin cambios. Si necesita cambiar el estado de un objeto o la relación porque sabe que su objeto se modificó en estado separado, use uno de los siguientes métodos.

Los métodos mencionados son ChangeObjectState y ChangeRelationshipState de ObjectStateManager = seguimiento manual de cambios. Una propuesta similar se encuentra en otro artículo de documentación de MSDN: Defining and Managing Relationships dice:

Si está trabajando con objetos desconectados, debe administrar manualmente la sincronización.

Además, hay una publicación de blog relacionada con EF v1 que critica exactamente este comportamiento de EF.

Razón de la solución

EF tiene muchas operaciones "útiles" y la configuración como de actualización , de carga , applyCurrentValues , ApplyOriginalValues , MergeOption etc, pero por mi investigación todas estas operaciones de trabajo sólo para una sola entidad y afecta sólo a preperties escalares (= no en las propiedades de navegación y relaciones). Prefiero no probar estos métodos con tipos complejos anidados en la entidad.

Otra solución propuesta

En lugar de la funcionalidad Merge real, el equipo de EF proporciona algo llamado Entidades de seguimiento automático (STE) que no resuelven el problema. En primer lugar, STE funciona solo si se utiliza la misma instancia para todo el procesamiento. En la aplicación web no es el caso a menos que almacene la instancia en el estado de vista o sesión. Debido a eso, no estoy contento con el uso de EF y voy a verificar las características de NHibernate. La primera observación dice que NHibernate quizás tenga tal funcionalidad .

Conclusión

Terminaré con estas suposiciones con un solo enlace a otra pregunta relacionada en el foro de MSDN. Compruebe la respuesta de Zeeshan Hirani. Es autor de Entity Framework 4.0 Recipes . Si dice que no se admite la fusión automática de gráficos de objetos, le creo.

Pero aún existe la posibilidad de que esté completamente equivocado y exista alguna funcionalidad de fusión automática en EF.

Edición 2:

Como puede ver, esto ya se agregó a MS Connect como sugerencia en 2007. MS lo ha cerrado como algo que se debe hacer en la próxima versión, pero en realidad no se ha hecho nada para mejorar esta brecha, excepto STE.

Ladislav Mrnka
fuente
7
Esta es una de las mejores respuestas que he leído en SO. Ha indicado claramente lo que tantos artículos de MSDN, documentación y publicaciones de blog sobre el tema no lograron transmitir. EF4 no admite de forma inherente la actualización de relaciones de entidades "independientes". Solo proporciona herramientas para que lo implemente usted mismo. ¡Gracias!
tyriker
1
Entonces, después de unos meses, ¿qué tal el NHibernate relacionado con este problema en comparación con EF4?
CallMeLaNN
1
Esto está muy bien soportado en NHibernate :-) no es necesario fusionar manualmente, en mi ejemplo es un gráfico de objetos profundos de 3 niveles, la pregunta tiene respuestas, cada respuesta tiene comentarios y la pregunta también tiene comentarios. NHibernate puede persistir / fusionar su gráfico de objetos, sin importar cuán complejo sea ienablemuch.com/2011/01/nhibernate-saves-your-whole-object.html Otro usuario satisfecho de NHibernate: codinginstinct.com/2009/11/…
Michael Buen
2
¡Una de las mejores explicaciones que he leído! Muchas gracias
marvelTracker
2
El equipo de EF planea abordar este post-EF6. Es posible que desee votar por entityframework.codeplex.com/workitem/864
Eric J.
19

Tengo una solución al problema que fue descrito anteriormente por Ladislav. He creado un método de extensión para DbContext que realizará automáticamente la función de agregar / actualizar / eliminar en función de una diferencia del gráfico proporcionado y el gráfico persistente.

Actualmente, utilizando Entity Framework, deberá realizar las actualizaciones de los contactos manualmente, verificar si cada contacto es nuevo y agregar, verificar si está actualizado y editar, verificar si se eliminó y luego eliminarlo de la base de datos. Una vez que tiene que hacer esto para algunos agregados diferentes en un sistema grande, comienza a darse cuenta de que debe haber una forma mejor y más genérica.

Eche un vistazo y vea si puede ayudar http://refactorthis.wordpress.com/2012/12/11/introducing-graphdiff-for-entity-framework-code-first-allowing-automated-updates-of-a- gráfico-de-entidades-separadas /

Puede ir directamente al código aquí https://github.com/refactorthis/GraphDiff

Brentmckendrick
fuente
Estoy seguro de que puedes resolver esta pregunta fácilmente, lo estoy pasando mal.
Shimmy Weitzhandler
1
Hola Shimmy, lo siento, finalmente tuve tiempo para echar un vistazo. Lo investigaré esta noche.
brentmckendrick
¡Esta biblioteca es genial y me ha ahorrado MUCHO tiempo! ¡Gracias!
lordjeb
9

Sé que es tarde para el OP, pero dado que este es un problema muy común, publiqué esto en caso de que le sirva a otra persona. He estado jugando con este problema y creo que tengo una solución bastante simple, lo que hago es:

  1. Guarde el objeto principal (blogs, por ejemplo) estableciendo su estado en Modificado.
  2. Consultar en la base de datos el objeto actualizado, incluidas las colecciones que necesito actualizar.
  3. Consultar y convertir .ToList () las entidades que quiero que incluya mi colección.
  4. Actualice la (s) colección (s) del objeto principal a la Lista que obtuve del paso 3.
  5. Guardar cambios();

En el siguiente ejemplo, "dataobj" y "_categories" son los parámetros recibidos por mi controlador "dataobj" es mi objeto principal, y "_categories" es un IEnumerable que contiene los ID de las categorías que el usuario seleccionó en la vista.

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

Incluso funciona para múltiples relaciones.

c0y0teX
fuente
7

El equipo de Entity Framework es consciente de que se trata de un problema de usabilidad y planea solucionarlo después de EF6.

Del equipo de Entity Framework:

Este es un problema de usabilidad del que somos conscientes y es algo en lo que hemos estado pensando y planeamos trabajar más en post-EF6. He creado este elemento de trabajo para realizar un seguimiento del problema: http://entityframework.codeplex.com/workitem/864 El elemento de trabajo también contiene un enlace al elemento de voz del usuario para esto. Le animo a que vote por él si tiene no lo he hecho ya.

Si esto le afecta, vote por la función en

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

Eric J.
fuente
post-EF6? ¿Qué año será entonces en el caso optimista?
quetzalcoatl
@quetzalcoatl: Al menos está en su radar :-) EF ha recorrido un largo camino desde EF 1, pero aún le queda mucho camino por recorrer.
Eric J.
1

Todas las respuestas fueron excelentes para explicar el problema, pero ninguna de ellas realmente me resolvió el problema.

Descubrí que si no usaba la relación en la entidad principal, sino que simplemente agregaba y eliminaba las entidades secundarias, todo funcionaba bien.

Lo siento por el VB, pero eso es en lo que está escrito el proyecto en el que estoy trabajando.

La entidad padre "Informe" tiene una relación de uno a muchos con "ReportRole" y tiene la propiedad "ReportRoles". Los nuevos roles se transmiten mediante una cadena separada por comas de una llamada Ajax.

La primera línea eliminará todas las entidades secundarias, y si usara "report.ReportRoles.Remove (f)" en lugar de "db.ReportRoles.Remove (f)", obtendría el error.

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
fuente