Introducción
Analizaremos como implementar las acciones que permiten eliminar una entidad y sus relaciones, pudiendo además por medio de las opciones de EF cambiar el mapping de la entidad para lograr cambiar las opciones de eliminar en cascada entre las tablas.
Otro tema que veremos es como asociar las entidades a través de su instancia. Hasta el momento la relación de una entidad con otra existente se realiza asignando la propiedad ID, o sea si un producto tiene una relación con una categoría se asigna la propiedad CategoryID, cuando se asignaba la propiedad Category de la entidad Product se creaba una nueva categoría lo cual puede que no se quiere que suceda.
Vamos a analizar de que forma podemos modificar el código para indicar que entidad asociada no se debe volver a crear.
En resumen los temas que se trataran serán:
- Eliminar en cascada
- Asociar mediante la instancia de la entidad
Este artículo es la continuación de:
[Entity Framework][Code First] Asociación uno a mucho (1/3)
1- Eliminar en cascada
Al seguir las convenciones en la definición de relaciones EF determina las acción de eliminar en cascada al crear las relaciones entre las tablas.
Si definimos una relación como obligatoria marcara automáticamente para permitir eliminar en cascada, salvo que se indique lo contrario.
En cambio si definimos una relación como opcional mediante la definición de un tipo null (en la propiedad asignada para el Foreign Key) en este caso EF no especificara ninguna acción.
1.1- Asociación obligatoria – Eliminar en cascada
Si definimos las entidades usando
en este caso por seguir las convenciones podríamos o no definir la línea en el context
HasRequired(x => x.Category) .WithMany(x => x.Products) .HasForeignKey(x => x.CategoryID)
se crearan las tablas relacionándolas con al opción de cascada activa
si ejecutamos un test como ser
[TestMethod] public void Delete_Cascade_Category() { ProductRepository repoProduct = new ProductRepository(); CategoryRepository repoCategory = new CategoryRepository(); //se crea la categoria Category categoryNew = new Category() { CategoryName = "category1", Description = "desc category 1" }; repoCategory.Create(categoryNew); //se crea el producto relacionado con la categoria Product productNew1 = new Product() { ProductName = "prod 1", UnitPrice = 10, Discontinued = false, CategoryID = categoryNew.CategoryID }; repoProduct.Create(productNew1); Product productNew2 = new Product() { ProductName = "prod 2", UnitPrice = 12, Discontinued = false, CategoryID = categoryNew.CategoryID }; repoProduct.Create(productNew2); //elimina la categoria y sus productos asociados repoCategory.Delete(categoryNew); //se recupera la categoria para validar si se elimino var categorySelected = repoCategory.Single(x => x.CategoryID == categoryNew.CategoryID); Assert.IsNull(categorySelected); //se recupera el primer producto Product productSel1 = repoProduct.Single(x=>x.ProductID == productNew1.ProductID); Assert.IsNull(productSel1); //se recupera el primer producto Product productSel2 = repoProduct.Single(x=>x.ProductID == productNew2.ProductID); Assert.IsNull(productSel2); }
se crea la categoría y sus dos productos relacionados
y la operación de elimina
pero al estar activa en la relación la opción de cascada se eliminan los productos, por eso si analizamos el test veremos que tanto la categoría como los productos ya no existen
1.2- Asociación obligatoria – Anular eliminar en cascada
Con el uso del método WillCascadeOnDelete() en la definición del contexto podremos cambiar la convención utilizada por EF cuando crea las relaciones entre las tablas.
al ejecutar el mismo test del caso anterior, obtendremos un error
por lo que ya no podremos eliminar la categoría si previamente no quitamos los productos asociados
1.3- Asociación opcional – Eliminar
Al definir la propiedad de relación como nullable EF interpretara que una asociación como opcional
por seguir las convenciones podríamos o no definir la línea en el context
HasOptional(x => x.Supplier) .WithMany(x => x.Products) .HasForeignKey(x => x.SupplierID);
se creara las relaciones entre las tablas en donde no se realiza ninguna acción al eliminar
Podemos validarlo por medio de un test
[TestMethod] public void Delete_Supplier() { ProductRepository repoProduct = new ProductRepository(); SupplierRepository repoSupplier = new SupplierRepository(); //se crea el proveedor Supplier supplierNew = new Supplier() { CompanyName = "supplier 1" }; repoSupplier.Create(supplierNew); //se crea el producto relacionandolo con el proveedor Product productNew1 = new Product() { ProductName = "prod 1", UnitPrice = 10, Discontinued = false, Category = new Category() { CategoryName = "category1", Description = "desc category 1" }, SupplierID = supplierNew.SupplierID }; repoProduct.Create(productNew1); //se crea otro producto relacionandolo con el proveedor Product productNew2 = new Product() { ProductName = "prod 2", UnitPrice = 12, Discontinued = false, Category = new Category() { CategoryName = "category2", Description = "desc category 2" }, SupplierID = supplierNew.SupplierID }; repoProduct.Create(productNew2); //se recupera el proveedor para obtener los productos asociados var supplierSel = repoSupplier.Single(x => x.SupplierID == supplierNew.SupplierID, new List<Expression<Func<Supplier, object>>>() { x => x.Products }); //se elimina la categoria y se desasocian los productos repoSupplier.Delete(supplierSel); //se recupera la categoria para validar si se elimino var categorySelected = repoSupplier.Single(x => x.SupplierID == supplierNew.SupplierID); Assert.IsNull(categorySelected); //se recupera el primer producto Product productSel1 = repoProduct.Single(x => x.ProductID == productNew1.ProductID); Assert.IsNotNull(productSel1); //se recupera el primer producto Product productSel2 = repoProduct.Single(x => x.ProductID == productNew2.ProductID); Assert.IsNotNull(productSel2); }
Durante la ejecución se puede ir verificando las acciones que se realizan en la db, se crea el proveedor y los productos
se recupera el proveedor y sus productos, esto es necesario ya que al realizar la eliminación los productos deben ser actualizados
ante de eliminar el proveedor se ejecuta el UPDATE que asigna null en el campo SupplierID para quitar la asociación, para luego si poder eliminar el registro del proveedor sin conflicto
la validación en el test confirma que el proveedor ya no existe, al recuperarlo se obtiene un null, no así con los productos que siguen existiendo
2 - Asociar mediante la instancia de la entidad
Si tenemos la instancia de una entidad y la asignamos a la propiedad de otro para relacionarla, al persistir no solo creara la entidad principal sino que creara una nuevo registro de la entidad que asignamos.
Si tenemos un código que hiciera esto:
ProductRepository repoProduct = new ProductRepository(); CategoryRepository repoCategory = new CategoryRepository(); SupplierRepository repoSupplier = new SupplierRepository(); //se crea la categoria Category categoryNew = new Category() { CategoryName = "category1", Description = "desc category 1" }; repoCategory.Create(categoryNew); //se crea un proveedor Supplier supplierNew = new Supplier() { CompanyName = "Company 1", }; repoSupplier.Create(supplierNew); //se crea el producto relacionado con la categoria Product productNew = new Product() { ProductName = "prod 1", UnitPrice = 10, Discontinued = false, Category = categoryNew, Supplier = supplierNew }; repoProduct.Create(productNew);
Debe observarse particularmente como se crean las entidades satélite y luego se asignan estas a las propiedades Category y Supplier de la entidad Product, hacer esto resultara en las consultas siguientes:
como se observa en una primer acción se crean las entidades Category y Supplier, pero al crear el Product nuevamente se vuelven a insertar un nuevo Category y Supplier que termina relacionado
Esto sucede porque en el código del repositorio cuando adjuntamos la entidad Product al contexto indicamos que es una nueva entidad, pero las instancias de los objetos relacionados no le decimos nada, por lo que EF deduce que estas entidades también deben crearse.
La solución rápida a este problema se consigue haciendo uso de las propiedades que definen el Foreign Key y no utilizar las propiedades definidas como clase, o sea utilizar:
Pero vamos a ver que puede haber otra alternativa si adaptamos la clase del repositorio pudiendo indicar que propiedades deben cambiar de estado.
2.1- Marcar entidades relacionadas con Unchanged
Que sucede si aun así queremos asignar las instancias, existe un solución pero requiere que desde el repositorio asignemos el “State” del contexto como “Unchanged”.
Para lograrlo vamos a necesitar que Reflection nos ayude a recuperar el objeto que se asigna a la propiedad, desde el repositorio implementaremos.
public virtual void Create(T entity, List<Expression<Func<T, object>>> unchangeProp) { // se obtiene la lista de propiedades que deben marcarse con el estado Unchanged List<string> unchangelist = unchangeProp.Select(x => ((MemberExpression)x.Body).Member.Name).ToList(); using (NorthWindContext context = new NorthWindContext()) { context.Set<T>().Add(entity); if (unchangeProp != null) { // se toma la instancia del objeto que esta asignada a la propiedad // y se asigna el estodo Unchanged foreach (string property in unchangelist) { PropertyInfo propertyInfo = typeof(T).GetProperty(property); var value = propertyInfo.GetValue(entity, null); context.Entry(value).State = EntityState.Unchanged; } } context.SaveChanges(); } }
El parámetro “unchangeProp” nos permite definir las propiedades asignadas y no queremos que la entidad sea creadas nuevamente.
Desde el código del test tendremos que definir que propiedades participaran
[TestMethod] public void Create_Unchange_CategoryAndSupplier_Product() { ProductRepository repoProduct = new ProductRepository(); CategoryRepository repoCategory = new CategoryRepository(); SupplierRepository repoSupplier = new SupplierRepository(); //se crea la categoria Category categoryNew = new Category() { CategoryName = "category1", Description = "desc category 1" }; repoCategory.Create(categoryNew); //se crea un proveedor Supplier supplierNew = new Supplier() { CompanyName = "Company 1", }; repoSupplier.Create(supplierNew); //se crea el producto relacionado con la categoria Product productNew = new Product() { ProductName = "prod 1", UnitPrice = 10, Discontinued = false, Category = categoryNew, Supplier = supplierNew }; repoProduct.Create(productNew, new List<Expression<Func<Product, object>>>() { x => x.Category, x => x.Supplier }); //se recupea el producto con la categoria y proveedor asociado var productSelected = repoProduct.Single(x => x.ProductID == productNew.ProductID, new List<Expression<Func<Product, object>>>() { x => x.Category, x => x.Supplier }); Assert.IsNotNull(productSelected.Category); Assert.AreEqual(productSelected.Category.CategoryID, productNew.CategoryID); Assert.IsNotNull(productSelected.Supplier); Assert.AreEqual(productSelected.Supplier.SupplierID, productNew.SupplierID); }
de todo el test el punto importante donde hay que enfocarse seria:
allí es donde se especifica las propiedades que deben asignarse como “Unchanged”
Al ejecutar el test veremos que solo se crea el INSERT del Product y no de las entidades que se se asignaron como sucedía anteriormente.
Ahora si podemos trabajas con las propiedades de instancia para crear las relaciones sin el problema que se vuelvan a crear nuevamente cuando las entidades ya existe.
Código
El código se encuentra en la primer parte del artículo.
Hola Leandro, son muy interesantes y utiles tus publicaciones sobre EF...deberias publicar algo recompilando todo ello, seria muy bueno! Ahora una pregunta...suponiendo que prefiera primero realizar mi base da datos y administrarla de la manera tradicional, que enfoque me recomiendas para trabajar en EF con ella, el Database-First o el Code-First .
ResponderEliminarTe agradeceria mucho tu opinion personal, y me digas de que forma preferis trabajar vos, que la tenes tan clara, sobre todo para bases de datos grandes. Saludos y gracias!
Hola Leo! te hago un consulta mappee un sistema que tenia y un objeto articulo que tengo es bastante complejo, el ejemplo de creaacio del articulo de un test es este :
ResponderEliminarArticulo a1 = new Articulo()
{
Comentario = "",
Costo = 0,
Descripcion = "",
Importe = 0,
modelo = new Modelo()
{
categoria = new Categoria()
{
Nombre = "Categoria",
},
Nombre = "modelo",
},
Oficios = new List()
{
new Carpinteria()
{
Observacion="Carpinteria",
FechaFinalizacion =DateTime.Today,
FechaInicio = DateTime.Today,
Costo = 2,
CostoInstalacion = 2,
partes = new List()
{
new Parte()
{
madera = new Madera()
{
Nombre="Roble",
},
ubicacion = new Ubicacion()
{
Lugar = "Patas",
},
}
}
}
}
};
como deberia hacer la consulta para cargar todo, es decir, las partes la ubicacion, a madera ?
Desde ya gracias!
hola Patos
ResponderEliminarpodrias hacer uso del Include() para que puedas recuperar no solo el Articulo sino sus relaciones
aunque no se si prefieres hacer uso de lazy load y dejar que se recupere las relaciones cuando las vayas a utilizar
lo que no logre ver con claridad si esta "madera" es parte de una propeidad del articulo o esta dentro de otra entidad
se que se puede utilizar propiedades anidadas en el Include() pero no estoy seguro cuando estas son listas
saludos
hola Leandro
ResponderEliminares para preguntarte si sabes como hacer esto.
tengo una tabla PERSONAS
PERSONAS
PK PRS_ID
PRS_NOMBRE
PRS_APELLIDO
Fk PRS_PADRE_ID
Fk PRS_MADRE_ID
TENGO QUE mostrar el nombre y apellido de todas las personas junto con los nombres y apellidos de su madre y de su padre (incluyendo los que no tienen padre, madre o ninguno)
la consulta debe hacer referencia a la misma tabla...me imagino..., sabes como hacer una consulta sql que me muestre lo anterior.
gracias
hola Elessar
ResponderEliminares una consulta sql o un linq lo que quieres obtener ? lo planteo porque estas consultando en un articulo de entity framework y con este no se arma ningun sql, se define un linq
si es sql podrias usar el LEFT JOIN para unir la misma tabla con los key del padre u la madre
saludos
Hola Leandro, excelente secuencia de artículos para comprender Code First. Aprovecho para hacerte una pregunta que se relaciona con este post, porqué es necesario vaciar primero la lista de productos cargada en categoría antes de eliminarla o de lo contrario da un error?
ResponderEliminarSi en el test, luego de crear los productos, se recupera la categoría cargando estos productos asociados luego al hacer la eliminación se produce el error, como se puede solucionar esto, aparte, claro está de limpiar explícitamente la lista de productos?
Gracias
hola Nazario
ResponderEliminarlo que quieres eliminar es una categoria ? la cual tiene productos asociados
si es asi recuerda validar que este habilitada el borrado en cascada, cuando defines la relacion puedes especificar si habilitas o no la opcion de borrar en cascada
sino lo habilitas entonces vas a tener que eliminar cada producto o cambiar la asignacion y ponerlo en otra categoria antes de poder eliminarla
pero esto es asi en EF como asi tambien si usaras ado.net simple
saludos
Gracias Leandro por la pronta respuesta pero el problema está aun vivo. Realmente sí quiero eliminar la categoría con los productos asociados como haces en tu ejemplo pero cuando la categoría tiene cargados estos productos.
ResponderEliminarEste es un pedazo de tu ejemplo con mi cambio para recuperar primero los productos recién creados:
...
//se crea el producto relacionado con la categoria
Product productNew1 = new Product()
{
ProductName = "prod 1",
UnitPrice = 10,
Discontinued = false,
CategoryID = categoryNew.CategoryID
};
repoProduct.Create(productNew1);
Product productNew2 = new Product()
{
ProductName = "prod 2",
UnitPrice = 12,
Discontinued = false,
CategoryID = categoryNew.CategoryID
};
repoProduct.Create(productNew2);
//Si adiciono esta linea para recuperar los productos creados antes de la eliminación va en error.
categoryNew = repoCategory.Single(x => x.CategoryID == categoryNew.CategoryID, new List>>() { x => x.Products });
//elimina la categoria y sus productos asociados
repoCategory.Delete(categoryNew);
...
Por supuesto que he especificado la eliminación en cascada:
...
HasRequired(x => x.Category)
.WithMany(x => x.Products)
.HasForeignKey(x => x.CategoryID).WillCascadeOnDelete(true);
...
Que causa este error?
Gracias y saludos.
hola Nazario
ResponderEliminarel codigo en principio lo veo correcto
pero que mensaje de error recibes ? recuerda analizar el InnerException a veces aporta un poco mas sobre el problema
validaste en la db que se haya definido correctamente la eliminacion en cascada?
saludos
Hola Leandro.
ResponderEliminarEsta es la excepción.
Error en la operación: no se pudo cambiar la relación porque una o varias de las propiedades de clave externa no aceptan valores NULL. Cuando se realiza un cambio en una relación, la propiedad de clave externa relacionada se establece en un valor NULL. Si la clave externa no admite valores NULL, se debe definir una nueva relación, asignar otro valor NULL a la propiedad de clave externa o eliminar el objeto no relacionado.
Te confirmo que en el db se crea correctamente la regla de eliminación en cascada y funciona pero el error no es a nivel de db.
El problema no surge de tu ejemplo, me ha ocurrido en aplicaciones reales desarrolladas y lo he resuelto limpiando la lista a mano antes de eliminar la entidad contenedora pero no me quiero quedar con la duda de que exista una solución mas limpia.
Si te sirve en este link https://skydrive.live.com/redir?resid=11989B4D737AD20E!276&authkey=!ADDxzwcM8OUVaq4 está tu ejemplo con los cambios que te comentaba, al ejecutar el test de eliminación en cascada salta el error.
Gracias por las molestias que te has tomado.
Saludos.
Hola Leandro que tal? Estoy siguiendo éste tutorial, específicamente el punto 2.1 pero el problema que tengo es que debo pasarle una icollection que contiene los objetos que ya existen en la base y los que quiero asociar al objeto que estoy creando pero me da error en la última línea context.Entry(value).State = EntityState.Unchanged; la Exception es "El tipo List'1 no forma parte del contexto actual" tienes idea porque será? Cómo puedo solucionar ésto? Muchas gracias! Saludos, Jeny.
ResponderEliminarhola Jeny
ResponderEliminares que cuando trabajas con listas no puedes darle un estado a la propiedad de la lista directamente, sino que debes recorrer cada item de esa lista y a cada objeto que contenga asignar el estado
saludos
Muchas gracias Leandro, pude resolverlo. Saludos.
ResponderEliminarhttps://social.msdn.microsoft.com/Forums/office/en-US/c00809be-adf6-45f7-b008-4938f0988201/how-to-count-fields-data-in-a-row?forum=accessdev
ResponderEliminarreply to this
Que tal Leandro, he estado leyendo tus tutoriales y me han sido de mucha ayuda para comprender EF, no tengo muchas bases, es un codigo de otra persona pero me requirieron hacer unos cambios. Mi problema es al tratar de hacer login o register con MVC5 la consola de DNX me arroja el siguiente error:
ResponderEliminarSqlException: Invalid object name 'AspNetUserRoles'.
Cuando en mi Modelo y mi base de datos no existe esa tabla, esta se llama UserRoles solamente. Me podras dar un hilo de donde verificar esto?
en mi contexto tengo asignada una clase definida por mi, para el DBSet de UserRoles
public new DbSet UserRoles { get; set; }
lo tuve que sobreescribir ya que IdentityUserRoles tambien me agregaba columnas fantasma en los querys de SQL.
De antemano ,muchas gracias. Braulio
hola
ResponderEliminarEse mensaje esta indicando que quieres crear la estructura de Identity
Introduction to ASP.NET Identity
Este define su propio contexto de EF del cual deberias extender IdentityDbContext
Integrating ASP.NET Identity into Existing DbContext
saludos