Hoy vengo con un post de programación avanzada. Es un caso concreto que me he ido encontrando en los últimos proyectos .Net que he hecho. Voy a explicar algunos conceptos y truquillos de cómo trata el Entity Framework las relaciones too many – too many entre tablas.
El Entity Framework nos facilita enormemente las funcionalidades estándars de consulta, inserción y modificación de una base de datos, pero hay que conocer bien como maneja las relaciones para que no nos suponga una desventaja en lugar de una ventaja, y por mi experiencia las relaciones muchos a muchos en EF, a veces no son fáciles de tratar.
Empecemos haciendo una breve guía de cómo funciona.
Imaginemos que tenemos una base de datos donde estamos guardando información de cómics y sus autores. Sabemos que un cómic puede tener más de un autor y que un autor puede haber escrito o dibujado más de un cómic, por lo que un posible diagrama reducido de BBDD quedaría así.
Ahora bien,… el EF al mapear estas tablas genera el siguiente diagrama.
Cómo vemos, el EF detecta que es una relación muchos a muchos y en lugar de generar la tabla explosionada intermedia incorpora una colección en cada una de las clases haciendo referencia a la otra clase. El código generado sería el siguiente:
Insertar datos
Para insertar datos en la tabla intermedia podemos hacer lo siguiente: incorporar en las vistas una select multiple con el listado de autores o cómics (dependiendo de la vista en la que estemos) y enviarlo al controlador para que lo incorpore en la colección correspondiente de la clase. De esta forma añadiendo el nuevo autor o cómic, directamente se añadirán las relaciones. Veámos como sería con código:
Suponemos que queremos dar de alta un cómic y asignarle sus autores que previamente ya existen en la base de datos:
- En el Get de Create del controlador de cómics, incluiremos una lista con todos los autores de la base de datos
- En la vista Create de cómics añadiremos una select multiple que recogerá todos los autores que hemos pasado a través del ViewBag para que seleccionemos los que queremos relacionar con el nuevo cómic. (Lógicamente si existen muchos autores optaremos por otro componente o bien utilizaremos librerías o ajax para que nos facilite la búsqueda entre ellos)
Visualmente podríamos tener algo de este estilo: - Una vez seleccionado los autores que queremos añadirle al comic, los enviamos como una lista al controlador, donde los recogeremos, añadiremos a la colección de Autores de la clase Comics, haciendo de esta forma que cuando llamemos a su método Add, añadirá no sólo la información en la tabla de Comics, sino que también añadirá la relación con sus autores en la tabla ComicAutor:
- En base de datos obtendremos este resultado:
Bueno,… hasta ahora parece que todo bien y no es complicado,… funciona perfecto y nos ahorra tener que llamar explícitamente al método Add de la tabla ComicAutor.
Vamos a ver cómo funciona ahora el modificar con el EF
Modificar datos
Para el proceso de modificar datos, hay muchas formas de hacerlo (como en todo proceso de programación) pero la que suelo usar más a menudo sería similar al ya explicado para el create, que sería añadir en el controlador (en el Get Edit) una lista con todos los autores y pasarla a la vista para rellenar una select multiple. En el caso del Edit, además debemos pasar los autores que ya estan dados de alta en la tabla intermedia para poder hacer que en la vista salgan seleccionados. Para ello suelo añadir un Include que me devuelva la colección de los Autores en la propia clase de Comics que quiero editar. El código sería este:
- En el Get de Edit del controlador del cómic, incluiremos una lista con todos los autores de la base de datos y añadiremos en la obtención del comic el Include para que traiga los valores de la tabla ComicAutor
- En la vista Edit de cómics añadiremos una select multiple con la lista de todos los autores y una variable que añadirá el texto «selected» a los autores que ya están relacionados con el cómic para que aparezcan seleccionados.Visualmente quedaría así
- Imaginemos ahora que queremos hacer cambios en los autores tal y como se muestra en la imagen siguiente:
- En este caso nos encontramos con 3 situaciones diferentes: Hay que quitar un autor (Ino Asano), añadir otro (Tsugumi Oba) y mantener otro (Takeshi Obata).
En nuestra ingenuidad, y creyendo que la programación es lógica y casi automática podríamos pensar que una solución como la del Create bastaría para hacer esta modificación, y que el propio EF se encargaría de detectar qué es lo que tiene que quitar, añadir o mantener,… pero no, lo siento,… no es tan adivino.
La solución reflejada en la imagen siguiente no funciona, lo único que hace es modificar los cambios que realizamos en los datos de la clase Comics pero no modifica las relaciones con Autores, ya que sólo cambia el estado a modificado de la entidad Comic.Y entonces os preguntareis,… ¿cómo puedo cambiar el estado de los registros de la entidad ComicAutor si el EF no me ha creado una clase para ella?. Pues efectivamente éste es el quid de la cuestión y después de muchos batacazos y probar diferentes formas de hacerlo, lo encontré,… por eso decidí escribir este post. No os impacientéis que os lo cuento enseguida,… - El truco está en primero guardar los datos de la clase Comics y después volver a recuperarla con el Include de la colección de autores para tratar la colección y hacer las operaciones pertinentes para borrar, añadir o mantener los datos de ComicAutor.
Una de las formas sería comparar en el controlador la lista de autores antiguos, con la de los autores seleccionados en la vista y hacer operaciones entre ellas: los datos que sean comunes, mantenerlos,… si hay datos en la lista antigua que no se haya seleccionado en la nueva, quitarlos,… y si hay datos en la lista nueva que no estén en la antigua añadirlos.
Pero la verdad, es que yo soy de la ley del mínimo esfuerzo y me parece un poco engorroso el comparar listas en este caso, así que yo prefiero, borrar todas las relaciones que había anteriormente en la colección de Autores para ese cómic y añadir las nuevas que vienen seleccionadas desde la vista, tal y como muestro en la siguiente imagen:
OJO!! CASO ESPECIAL CON DISTINTO DBCONTEXT
En el ejemplo que estoy haciendo estoy trabajando con el dbContext en el propio controlador, si tenemos una arquitectura de software dividia en proyectos donde pasamos toda la lógica de acceso a datos a otro proyecto con otro dbContext, es posible que tengamos que hacer primero un Detach de cada uno de los Autores antes de poder borrar las relaciones con Comic. En la siguiente imagen muestro como hacer el Detach antes de borrar:
Y AQUÍ VIENE LO BUENO: TRUCO PARA ENGAÑAR AL ENTITY
Si te pasa como a mi que eres de la vieja escuela de programadores y te gusta tener el control de todas las tablas y no dejarle la gestión al Entity Framework, hay un truco para «engañarle» y que te cree la clase de la tabla intermedia, con lo que serás tú quien hagas cuando tú quieras y cómo quieras los inserts, updates y deletes.
El truco es tan fácil cómo añadirle una columna más a la tabla ComicAutor que no esté relacionada con ninguna otra tabla. En mi caso podría ser añadir un campo Función donde se pudiera poner que es lo que hace ese Autor en ese Cómic, por ejemplo si hace el guión, el dibujo, entintado…
También en muchos casos se podría añadir columnas de auditorías con el usuario que modifica y el timestamp de la modificación o creación,… o si no sabes que añadir, incluye simplemente una columna Dummy que no utilices para nada,… el caso es añadir algo,..
Con esta columna añadida lo que conseguimos es que el EF mapee las tablas con el siguiente diagrama:
Y cree las clases siguientes:
Y de esta forma tendrás acceso a todos los métodos de la clase ComicAutor para añadir, modificar, borrar y manejar los datos como quieras,….
Yo casi lloré con este descubrimiento,…. TT,… gracias a él ya no le tengo tanta manía a las relaciones muchos a muchos con EF.
Bueno, al final me ha salido un poco largo y denso este POST, pero espero que te ayude a entender más sobre este tema y a encontrar truquillos para manejarlos mejor.
Nos leemos en el siguiente
Technology is coming!!!
jonathan dice
muy bueno muy bien explicado
José Miquilena dice
hola, excelente post muy bien explicado. esto era lo esta buscando, yo estaba como tu partiendome el coco jejejeje.
saludos desde Venezuela
YARR Blog dice
Muchas gracias, me fue de muchas ayuda.
Byron dice
Me habría mucho gustado q pongas el ejemplo de hacer la eliminación por el camino largo.
Cómo garantizas consistencia en caso q se corte comunicación con la base si ya enviaste a realizar el Delete??
Alu dice
GENIO…gracias…
Hernan dice
Me ayudaste un Monton!!! Gracias Loco!!!!!
Andres Hernandez dice
Muy Buen Post.
Quisiera saber si es posible que en la tabla intermedia los datos que se almacene no sean los de la columna ID sino una columna que denomine CodUsuario, por decir un nombre. la cual representa un valor alfanumérico. Cual es el fin, si se borra el usuario y se crea nuevamente el ID cambia automáticamente porque es el incrementa, pero su código no cambiaría ya que es propio.
Gracias y saludos desde Venezuela.
Alvaro dice
De donde sale EFMenytoMeny?
admin dice
El EFManytoMany que sale en las vistas es el namespace de mi proyecto.
No tiene por qué ser ese, solo lo he puesto yo así para identificar este ejemplo.
Y lo añado en las vistas simplemente para que al poner el punto el programa me ofrezca la lista de posibilidades y de esa forma ir más rápido a la hora de programar (o no tener que pensar tanto).
Si pones using al inicio no hace falta que se añada, solo llamando a Autores o Comics sería bastante.
Espero haberte ayudado!
Jancer Fabian dice
Como serian los controladores y vistas al utilizar la ultima forma, donde se crea una clase intermedia ??
Hans Ulloa dice
excelente muy bien explicado
THEO LOEFFELMANN IBARRA dice
Sonaré machista quizás, pero que buen trabajo, no muchas mujeres entienden del desarrollo y menos dan una explicación igual, de los hombres mismo caso no lo explican tan bien, sirvió mucho, gracias
admin dice
Bueno,… tal vez es que ahora tenemos más oportunidades de demostrar que somos igual de capaces! ;-P 😜
Gracias por el comentario, me alegro que te haya ayudado.
Alex García dice
Excelente solución.. te pasaste! jaja
Patricio Mera dice
Hola y gracias por la explicación. Una duda, cuando se usa DB First se crean las dos clases (Autores y Comics) y a través de las propiedades de navegación se infiere a VS a crear la tabla intermedia. Si queremos agregar campos a esa tabla intermedia debemos crear la clase de la misma manualmente?
Saludos desde Ecuador
admin dice
Si utilizas db First, significa que ya tienes las tablas creadas. Por lo tanto en la tabla intermedia que ya está creada es donde le añades la columna nueva que no esté relacionada con las otras tablas.
Al hacer eso y crear el modelo desde la base de datos, ya te creará la clase de la tabla intermedia automáticamente de forma separada.
Si utilizas el model first, lo mismo,… creas en la definición del modelo las columnas de más y así tendrás la clase separada.
Espero haberte ayudado!
Esteban dice
De donde sale EFManytoMany?
admin dice
El EFManytoMany que sale en las vistas es el namespace de mi proyecto.
No tiene por qué ser ese, solo lo he puesto yo así para identificar este ejemplo.
Y lo añado en las vistas simplemente para que al poner el punto el programa me ofrezca la lista de posibilidades y de esa forma ir más rápido a la hora de programar (o no tener que pensar tanto).
Si pones using al inicio no hace falta que se añada, solo llamando a Autores o Comics sería bastante.
Espero haberte ayudado!
manu dice
por dios me soluciona la vida este post , sos una GENIA 🙂
Sergiii dice
Todo un crack maestro te felicito muy util muy bien expicado. Es una exelente referencia para i iniciantes. Saludos dease Argentina.
FERNANDO JAVIER VERA PÉREZ dice
Estuve 4 días atascado con la actualización de elementos, casi llego a tus conclusiones, pero me ahorraste otro día más, yo también casi lloro con esta solución jajajaja.
Gracias y suerte.