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.