jueves, 4 de julio de 2013

[Entity Framework][Code First] Asociación uno a muchos (3/3)

 

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:

  1. Eliminar en cascada
  2. 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

image

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

image

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

image

y la operación de elimina

image

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

 

SNAGHTML7ff1cd3d

 

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.

image

al ejecutar el mismo test del caso anterior, obtendremos un error

SNAGHTML7ffbf289

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

image

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

image

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

image

se recupera el proveedor y sus productos, esto es necesario ya que al realizar la eliminación los productos deben ser actualizados

image

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

image

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

image

 

 

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:

image

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:

image

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:

image

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.

image

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.

16 comentarios:

  1. 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 .
    Te 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!

    ResponderEliminar
  2. 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 :

    Articulo 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!

    ResponderEliminar
  3. hola Patos

    podrias 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

    ResponderEliminar
  4. hola Leandro
    es 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

    ResponderEliminar
  5. hola Elessar

    es 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

    ResponderEliminar
  6. 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?
    Si 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

    ResponderEliminar
  7. hola Nazario

    lo 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

    ResponderEliminar
  8. 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.
    Este 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.

    ResponderEliminar
  9. hola Nazario

    el 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

    ResponderEliminar
  10. Hola Leandro.
    Esta 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.

    ResponderEliminar
  11. 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.

    ResponderEliminar
  12. hola Jeny

    es 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

    ResponderEliminar
  13. Muchas gracias Leandro, pude resolverlo. Saludos.

    ResponderEliminar
  14. https://social.msdn.microsoft.com/Forums/office/en-US/c00809be-adf6-45f7-b008-4938f0988201/how-to-count-fields-data-in-a-row?forum=accessdev

    reply to this

    ResponderEliminar
  15. 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:

    SqlException: 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

    ResponderEliminar
  16. hola
    Ese 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

    ResponderEliminar