domingo, 28 de julio de 2013

[Entity Framework][Code First] Asociación mucho a muchos

 

Introducción


Al momento de relacionar entidades algunas pueden necesitar asociarse con colecciones de otras entidades, pero que sucede cuando esto se necesita en ambas direcciones?, es aquí donde entran las relaciones mucho a muchos, en donde a nivel de base de datos se va a requerir de una tabla intermedia para poder implementar la relación.

En este articulo veremos como lograr esta asociación y analizaremos que pasa a nivel de base de datos, el objetivo final será lograr un modelo de objeto como el siguiente:

image

y su equivalente en el modelo de datos:

image

 

Definición de las entidades


En el proyecto “Entities” serán definidas las clases que modelan los empleados y territorios, cada uno define una propiedad del tipo colección hacia a otra.

 

public class Territory
{
    public int TerritoryID { get; set; }
    public string TerritoryDescription { get; set; }

    public virtual ICollection<Employee> Employees { get; set; }

}
public class Employee
{

    public int EmployeeID { get; set; }

    public string LastName { get; set; }
    public string FirstName { get; set; }

    public string Address { get; set; }
    public string City { get; set; }
    public string Region { get; set; }
    public string PostalCode { get; set; }
    public string Country { get; set; }

    public virtual ICollection<Territory> Territories { get; set; }
}

 

Definición del contexto


La clase de context, la cual hereda de Dbcontext, define las propiedades que representan cada entidad, utilizando el tipo DbSet<>.

La sobrecarga del método OnModelCreating será el responsable de la configuración del mapping, para que no quede toda el código junto en un solo sitio y sea mantenible en el tiempo se hará uso de clases separadas para cada entidad, estas clases heredan de EntityTypeConfiguration<>.

 

public class NorthWindContext : DbContext
{

    public NorthWindContext()
        : base("NorthwindDb")
    {
        this.Configuration.LazyLoadingEnabled = false;
        this.Configuration.ProxyCreationEnabled = false;
    }

    public DbSet<Territory> Territories { get; set; }
    public DbSet<Employee> Employees { get; set; }

    protected override void OnModelCreating(DbModelBuilder modelBuilder)
    {
        modelBuilder.Configurations.Add(new TerritoryMap());

        modelBuilder.Configurations.Add(new EmployeeMap());

        base.OnModelCreating(modelBuilder);
    }

}

En este caso se decidió utilizar la entidad del territorio para definir la relación mucho a muchos, esto se logra definiendo el HasMany()

La relación requiere definir el nombre que tendrá la tabla intermedia, así como los campos claves que actuaran en la relación.

 

public class TerritoryMap : EntityTypeConfiguration<Territory>
{
    public TerritoryMap()
    {
        HasKey(x => x.TerritoryID);
        Property(x => x.TerritoryDescription).HasColumnType("nchar").HasMaxLength(50).IsRequired();

        HasMany(x => x.Employees)
            .WithMany(x => x.Territories)
            .Map(mc =>
            {
                mc.MapLeftKey("TerrytoryID");
                mc.MapRightKey("EmployeeID");
                mc.ToTable("EmployeeTerritories");
            });
    }
}

Nota: la definición de la relación no era obligatorio hacerlo en la entidad Territory, se podría haber realizado en la clase Map del Employee sin ningún problema, solo hay que tener en cuenta cambiaran las definiciones de MapLeftKey() y MapRighKey()

El empleado define el mapeo normalmente

public class EmployeeMap : EntityTypeConfiguration<Employee>
{
    public EmployeeMap()
    {
        HasKey(x => x.EmployeeID);
        Property(x => x.LastName).HasMaxLength(20).IsRequired();
        Property(x => x.FirstName).HasMaxLength(10).IsRequired();

        Property(x => x.Address).HasMaxLength(60);
        Property(x => x.City).HasMaxLength(15);
        Property(x => x.Region).HasMaxLength(15);
        Property(x => x.PostalCode).HasMaxLength(10);
        Property(x => x.Country).HasMaxLength(15);
    }
}

Especialización del repositorio


Para poder operar con las colecciones se requiere estar dentro del contexto de EF, pero sucede que la implementación del repositorio desconecta las instancias de las entidades al devolverás para poder ser usadas desde fuera por lo tanto aplicar cambios en estas no ejecutara las acciones de actualización de forma correcta.

Es aquí donde se requiere personalizar el repositorio, por ejemplo en el repositorio del territorio definir métodos que permiten agregar o quitar empleados.

Si bien los parámetros de estos métodos son tipos de cada entidad, se podrían haber utilizado solo los IDs de las mismas

 

public class TerritoryRepository : BaseRepository<Territory>
{

    public void AddEmployees(Territory territory, List<Employee> employes)
    {
        using (NorthWindContext context = new NorthWindContext())
        {
            //marcamos el territorio para que no reciba cambios
            context.Entry(territory).State = EntityState.Unchanged;

            if (territory.Employees == null)
                territory.Employees = new List<Employee>();

            //recorremos cada empleado que se quiera asociar 
            employes.ForEach(x =>
            {
                //el empleado tampoco debe recibir cambios
                context.Entry(x).State = EntityState.Unchanged;
                //asociamos a la colecion de empleados del territorio el nuevo item
                //este si recibira cambios
                territory.Employees.Add(x);
            });
            
            context.SaveChanges();
        }
    }

    public void RemoveEmployees(Territory territory, List<Employee> employees)
    {
        //validamos que haya algo que remover
        if (employees == null || employees.Count == 0)
            return;

        using (NorthWindContext context = new NorthWindContext())
        {
            //recuperamos el terrotorio y sus empleados
            //esto es necesario porque el objeto donde se debe remover tiene que estar dentro del contexto de EF
            Territory territorySel = context.Set<Territory>().Include("Employees").FirstOrDefault(x => x.TerritoryID == territory.TerritoryID);

            if (territory.Employees == null || territory.Employees.Count == 0)
                return;

            employees.ForEach(x =>
                {
                    //localizamos al empleado dentro de la coleccion que se recupero anteriormente
                    Employee employeeRemove = territorySel.Employees.First(e => e.EmployeeID == x.EmployeeID);
                    //se remueve de la coleccion haciendo uso de la instancia
                    territorySel.Employees.Remove(employeeRemove);
                });

            context.SaveChanges();
        }
    }

}

Test – Obtener territorio con empleados asociados


Empezaremos con un test simple, en donde solo se relacionen las entidades y puedan recuperarse

[TestMethod]
public void Get_ListEmployee_Territory()
{
    TerritoryRepository repoTerritory  = new TerritoryRepository();

    Territory territoryNew = new Territory()
    {
        TerritoryDescription = "territoty 1",
        Employees = new List<Employee>()
        {
            new Employee() { FirstName = "Name 1", LastName ="LastaName 1" },
            new Employee() { FirstName = "Name 2", LastName ="LastaName 2" }
        }
    };
    repoTerritory.Create(territoryNew);

    var territorySel = repoTerritory.Single(x => x.TerritoryID == territoryNew.TerritoryID, 
                                                new List<Expression<Func<Territory,object>>>(){ x=>x.Employees });

    Assert.IsNotNull(territorySel);
    Assert.IsNotNull(territorySel.Employees);
    Assert.AreEqual(territorySel.Employees.Count, 2);

}

La creación del territorio implicara varias acciones como puede observarse en la captura del profiler

image

Se insertan los empleados, a continuación el territorio y por ultimo los registros que relacionan ambas entidades

Para recuperar las relaciones es necesario definir la propiedad como parte del Include()

image

En una sola instrucción SELECT se recuperan el territorio y los empleados asociados a esta.

 

Test – Agregar empleados a territorio existentes


Seria muy raro que al momento de crear un territorio se disponga de los empleados que se asocian al mismo, y solo estos conformen la relación, lo común es tener entidades independientes y asociarlas de forma dinámica, por lo general mediante acciones del usuario en la aplicación.

 

[TestMethod]
public void Update_ExistingEmployee_Territory()
{
    TerritoryRepository repoTerritory = new TerritoryRepository();
    EmployeeRepository repoEmployee = new EmployeeRepository();

    //se crean los empleados
    Employee employeeNew1 = new Employee() { 
        FirstName = "Name 1", 
        LastName ="LastaName 1" 
    };
    repoEmployee.Create(employeeNew1);

    Employee employeeNew2 = new Employee() { 
        FirstName = "Name 2", 
        LastName ="LastaName 2" 
    };
    repoEmployee.Create(employeeNew2);

    //se crea el territorio
    Territory territoryNew = new Territory()
    {
        TerritoryDescription = "territoty 1"
    };
    repoTerritory.Create(territoryNew);

    //asignamos los empleados al territorio existente
    repoTerritory.AddEmployees(territoryNew, new List<Employee>(new Employee[]{ employeeNew1, employeeNew2 }));

    //validamos que la asignacion se haya realizado correctamente 
    //recuperando la entidad y sus relaciones
    var territorySel = repoTerritory.Single(x => x.TerritoryID == territoryNew.TerritoryID,
                                                new List<Expression<Func<Territory, object>>>() { x => x.Employees });

    Assert.IsNotNull(territorySel);
    Assert.IsNotNull(territorySel.Employees);
    Assert.AreEqual(territorySel.Employees.Count, 2);


}

La primer parte de la ejecución del test crea las entidades de forma independiente

image

Luego se añade los empleados al territorio

image

y al final para validar que todo este correcto recuperando el territorio y su relación con los empleados

image

 

Test – Remover un empleado asociado


Hasta el momento hemos agregando entidades a la colección, pero es muy común querer quitarlas, para esto haremos uso del método creado especialmente en el repositorio de la entidad Territorio.

Antes de seguir hay que aclarar que la estructura de las tablas que crea Entity Framework desde el modelo asigna en las relaciones la opción de borrado en cascada, es necesario conocer esto si es que se quiere eliminar una entidad completa, ya que el hacerlo provocara que todas las asociaciones que esta tenga también sean eliminadas

 

image

 

image

Veamos el código del test que remueve un empleado en concreto de la colección

[TestMethod]
public void Delete_AssignedEmployee_Territory()
{
    TerritoryRepository repoTerritory = new TerritoryRepository();
    EmployeeRepository repoEmployee = new EmployeeRepository();

    //se crean los empleados
    Employee employeeNew1 = new Employee()
    {
        FirstName = "Name 1",
        LastName = "LastaName 1"
    };
    repoEmployee.Create(employeeNew1);

    Employee employeeNew2 = new Employee()
    {
        FirstName = "Name 2",
        LastName = "LastaName 2"
    };
    repoEmployee.Create(employeeNew2);

    //se crea el territorio
    Territory territoryNew = new Territory()
    {
        TerritoryDescription = "territoty 1"
    };
    repoTerritory.Create(territoryNew);

    //asignamos los empleados al territorio existente
    repoTerritory.AddEmployees(territoryNew, new List<Employee>(new Employee[] { employeeNew1, employeeNew2 }));

    //validamos que la asignacion se haya realizado correctamente 
    //recuperando la entidad y sus relaciones
    var territorySel = repoTerritory.Single(x => x.TerritoryID == territoryNew.TerritoryID,
                                                new List<Expression<Func<Territory, object>>>() { x => x.Employees });

    Assert.IsNotNull(territorySel);
    Assert.IsNotNull(territorySel.Employees);
    Assert.AreEqual(territorySel.Employees.Count, 2);

    //removemos uno de los empleados asignados
    repoTerritory.RemoveEmployees(territoryNew, new List<Employee>(new Employee[] { employeeNew1 }));

    //recuperamos el territorio para validar que se haya eliminado el empleado 
    var territorySel2 = repoTerritory.Single(x => x.TerritoryID == territoryNew.TerritoryID,
                                                new List<Expression<Func<Territory, object>>>() { x => x.Employees });

    Assert.IsNotNull(territorySel2);
    Assert.IsNotNull(territorySel2.Employees);
    Assert.AreEqual(territorySel2.Employees.Count, 1);

    Employee employeeSel = territorySel2.Employees.First();
    Assert.AreEqual(employeeSel.FirstName, employeeNew2.FirstName);
    Assert.AreEqual(employeeSel.LastName, employeeNew2.LastName);
}

La primer parte es idéntica al Test anterior, se crean las entidades individualmente, se asignan los empleados al territorio y se validad que todo se haya realizado correctamente.

Luego se procede a quitar uno de los empleados, ejecutándose la instrucción SQL que remueve el registro

image

 

Documentación de referencia


Associations in EF Code First: Part 6 – Many-valued Associations

Configuring a Many-to-Many Relationship

 

Código


Se utilizo Visual Studio 2012, no se adjunta ninguna base de datos, esta será creada al ejecutar los test utilizando el modelo definido en el contexto de EF.

[C#]
 

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.

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

 

Introducción


En esta segunda parte del artículo se ejecutaran varios Test que permitirán analizaran las consultas que Entity Framework generara contra la base de datos.

La primer parte del artículo define las entidades que aquí se validaran con los test.

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

 

Test – Crear Producto con categorías y proveedores existentes


Crearemos de forma individual la entidad de categorías y proveedores para luego asociarlas al producto.

[TestMethod]
public void GetSingle_WithCategoryAndSupplier_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 prod = new Product()
    {
        ProductName = "prod 1",
        UnitPrice = 10,
        Discontinued = false,
        CategoryID = categoryNew.CategoryID,
        SupplierID = supplierNew.SupplierID
    };
    repoProduct.Create(prod);

    //se recupea el producto con la categoria y proveedor asociado
    var productSelected = repoProduct.Single(x => x.ProductID == prod.ProductID,
                                            new List<Expression<Func<Product, object>>>() { x => x.Category, x=>x.Supplier });

    Assert.IsNotNull(productSelected.Category);
    Assert.AreEqual(productSelected.Category.CategoryID, categoryNew.CategoryID);
    
    Assert.IsNotNull(productSelected.Supplier);
    Assert.AreEqual(productSelected.Supplier.SupplierID, supplierNew.SupplierID);
}

 

Se instancias los repositorios para cada una de las entidades intervinientes y se crean las entidades de forma individual.

Por medio de la propiedad que representan la Foreing Key es que logramos la asociación entre las entidades. Cada operación genera en la db las siguientes operaciones de insert

image_thumb[31]

Al recuperar el producto y sus relaciones se debe especificar cuales se quiere traer en al definición del parámetro del include, esto genera la consulta:

image_thumb[21]

en una sola operación se recupera el producto, la categoría y el proveedor optimizando el acceso a la base de datos

 

Test – Crear Producto con categorías y proveedores NO existentes


Si se necesita en un único paso crear una entidad y sus relaciones se puede definir como el siguiente test

[TestMethod]
public void Create_WithCategoryAndSupplier_Product()
{
    ProductRepository repoProduct = new ProductRepository();

    //se crea el producto relacionado con la categoria
    Product productNew = new Product()
    {
        ProductName = "prod 1",
        UnitPrice = 10,
        Discontinued = false,
        Category = new Category()
                    {
                        CategoryName = "category1",
                        Description = "desc category 1"
                    },
        Supplier = new Supplier()
                    {
                        CompanyName = "Company 1",
                    }
    };
    repoProduct.Create(productNew);

    //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);
}

solo se utiliza un único repositorio creando las entidades asociadas directamente en la misma acción, al ejecutar en la db se registras las operaciones de insert por cada entidad

image_thumb[32]

a diferencia del test anterior aquí las operaciones se realizan una a continuación de otra, luego de la creación de las entidades estas tendrán asignado los Id que genera la db para todas las entidades, tanto la principal como para las relacionadas

el select que recupera el producto no sufre cambios comparado con el test anterior

image_thumb[33]

 

Test – Crear Categoría con Productos existentes


Crearemos el productos de forma independiente para después relacionar con la categoría.

[TestMethod]
public void GetSingle_AllProducts_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);


    var categorySelected = repoCategory.Single(x => x.CategoryID == categoryNew.CategoryID, 
                                                    new List<Expression<Func<Category,object>>>() { x=>x.Products });

    Assert.IsNotNull(categorySelected);
    Assert.IsNotNull(categorySelected.Products);
    Assert.AreEqual(categorySelected.Products.Count, 2);

}

Sino se define el include al recuperar la categoría la propiedad “Products” devolverá null

image

pero si se define el “include” se podrán obtener los productos relacionados a la entidad

image

se ejecuta un único select para recuperar la categoría y sus productos

image

el “include” optimiza la consulta generada por EF para recuperar las entidades

 

Test – Crear Categoría con Productos NO existentes


Se puede crear las entidades y asociarlas en una misma acción, pero debe recordarse que se crearan registros nuevos en la tabla para la entidad principal como para las relaciones

[TestMethod]
public void Create_WithProducts_Category()
{
    CategoryRepository repoCategory = new CategoryRepository();

    //se crea la categoria
    Category categoryNew = new Category()
    {
        CategoryName = "category1",
        Description = "desc category 1",
        Products = new List<Product>(){
            new Product()
                {
                    ProductName = "prod 1",
                    UnitPrice = 10,
                    Discontinued = false
                },
            new Product()
                {
                    ProductName = "prod 2",
                    UnitPrice = 12,
                    Discontinued = false
                }
        }
    };
    repoCategory.Create(categoryNew);

    var categorySelected = repoCategory.Single(x => x.CategoryID == categoryNew.CategoryID,
                                                    new List<Expression<Func<Category, object>>>() { x => x.Products });

    Assert.IsNotNull(categorySelected);
    Assert.IsNotNull(categorySelected.Products);
    Assert.AreEqual(categorySelected.Products.Count, 2);

}

La creación de los registros ejecutara varias instrucciones insert, luego de la creación del registro devolverá los Id generados en cada instrucción

image

al recuperar al entidad principal y su relación con la colección optimizara el select para realizarlo en una única operación

image

 

Código


El código se encuentra en la primer parte del artículo.

miércoles, 3 de julio de 2013

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

 

Introducción


Al hacer uso de entidades rara vez son simples y están aisladas, lo normal es que una entidad interactué con otras relacionándose por medio de propiedades que permiten navegar sus ítems.

Durante este artículo y los siguiente veremos como Entity Framework nos ayudara en la tarea de recuperar las asociaciones a otras entidades.

Empezaremos por la configuración simple, para luego especializar la configuración, analizaremos como afecta las opciones de lazy load al definir el repositorio. En el siguiente artículo analizaremos mediante la ejecución de Test las distintas formas de asociar entidades y como repercuten en las consultas que EF generara contra la db. En el ultimo artículo veremos como eliminar cascada y asociar las entidades mediante la asignación de la instancia en las propiedades.

 

Definición de las entidades


Empezaremos definiendo las entidades que formaran nuestro dominio

image

 

public class Product
{

    public int ProductID { get; set; }

    public string ProductName { get; set; }
    public string QuantityPerUnit { get; set; }
    public decimal? UnitPrice { get; set; }

    public short? UnitsInStock { get; set; }
    public short? UnitsOnOrder { get; set; }
    public short? ReorderLevel { get; set; }

    public bool Discontinued { get; set; }

    public int CategoryID { get; set; }
    public virtual Category Category { get; set; }

    public int? SupplierID { get; set; }
    public virtual Supplier Supplier { get; set; }
}

 

public class Category
{
    public int CategoryID { get; set; }

    public string CategoryName { get; set; }
    public string Description { get; set; }

    public virtual ICollection<Product> Products { get; set; }
}

 

public class Supplier
{
    public int SupplierID { get; set; }
    public string CompanyName { get; set; }

    public virtual ICollection<Product> Products { get; set; }
}

El entidad Product tiene asociación con Categoría (obligatoria) y Supervisor (opcional).

En este caso se estas siguiendo las convenciones, por lo que además de la propiedad que define la clase de navegación, se debe definir una propiedad adicional que identifique el campo que actuara como Foreign Key.

La obligatoriedad o no de una relación se define mediante la asignación de un tipo Nullable en la propiedad mencionada anteriormente (al permitir nulos la relación será opcional).

Las entidades Category y Supplier disponen de una propiedad de tipo colección que referencia al Product, pero esta propiedad puede ser opcional si es que no se desea recuperar una de las direcciones de la asociación.

 

Configuración estándar


Si solo definimos el contexto sin especificar ningún otro detalle.

public class NorthWindContext : DbContext
{

    public NorthWindContext()
        : base("NorthwindDb")
    {
        this.Configuration.LazyLoadingEnabled = false;
        this.Configuration.ProxyCreationEnabled = false;
    }

    public DbSet<Category> Categories { get; set; }
    public DbSet<Product> Products { get; set; }
    public DbSet<Supplier> Suppliers { get; set; }

    protected override void OnModelCreating(DbModelBuilder modelBuilder)
    {
       

        base.OnModelCreating(modelBuilder);
    }

}

 

Al ejecutar los test estos generaran la base de datos con la estructura de tablas como la siguiente:

image

Como se puede visualizar con casi nada de especificación se consigue un modelo relacional aceptable, solo se siguieron algunas convenciones:

- Definir las propiedades de navegación entre las entidades y con una adicional que representa la Foreign Key

- Utilizando null en la propiedad Foreign Key para indicar si es opcional

 

Especificación con Fluent API


Si bien las convenciones nos dejan un modelo de persistencia bastante cercano a las necesidades, cuando la definiciones escapan a las normas se puede especializar mediante configuración, especialmente cuando se utiliza una base de datos existente.

 

public class NorthWindContext : DbContext
{

    public NorthWindContext()
        : base("NorthwindDb")
    {
        this.Configuration.LazyLoadingEnabled = false;
        this.Configuration.ProxyCreationEnabled = false;
    }

    public DbSet<Category> Categories { get; set; }
    public DbSet<Product> Products { get; set; }
    public DbSet<Supplier> Suppliers { get; set; }

    protected override void OnModelCreating(DbModelBuilder modelBuilder)
    {
       
        modelBuilder.Configurations.Add(new CategoryMap());
        modelBuilder.Configurations.Add(new ProductMap());
        modelBuilder.Configurations.Add(new SupplierMap());

        base.OnModelCreating(modelBuilder);
    }

}


public class CategoryMap : EntityTypeConfiguration<Category>
{
    public CategoryMap()
    {
        ToTable("Categories");

        HasKey(c => c.CategoryID);
        Property(c => c.CategoryID).HasDatabaseGeneratedOption(DatabaseGeneratedOption.Identity);

        Property(c => c.CategoryName).IsRequired().HasMaxLength(15);
        Property(c => c.Description).HasColumnType("ntext");

    }
}


public class ProductMap : EntityTypeConfiguration<Product>
{
    public ProductMap()
    {
        ToTable("Products");

        HasKey(x => x.ProductID);
        Property(x => x.ProductID).HasDatabaseGeneratedOption(DatabaseGeneratedOption.Identity);

        Property(x => x.ProductName).HasMaxLength(40);
        Property(x => x.QuantityPerUnit).HasMaxLength(20);
        Property(x => x.UnitPrice).HasColumnType("money").IsOptional();

        HasRequired(x => x.Category).WithMany(x => x.Products).HasForeignKey(x => x.CategoryID);
        //HasRequired(x => x.Category).WithMany().HasForeignKey(x => x.CategoryID);

        HasOptional(x => x.Supplier).WithMany(x => x.Products).HasForeignKey(x => x.SupplierID);
    }
}

public class SupplierMap : EntityTypeConfiguration<Supplier>
{
    public SupplierMap()
    {
        HasKey(x => x.SupplierID);
        Property(x => x.CompanyName).HasMaxLength(40).IsRequired();

    }
}

 

Al ejecutar alguno de los test se generar la estructura de la db:

image

Si bien a nivel de base de datos las relaciones no cambiaron desde código se puedo definir el código que detalla el modelo de persistencia que queremos.

Para definir la relación obligatoria con la entidad Categoría se utiliza la línea:

HasRequired(x => x.Category).WithMany(x => x.Products).HasForeignKey(x => x.CategoryID);

en este caso se define que propiedades intervienen en la relación, por supuesto si se hace uso de las convenciones esta de mas esta declaración, pero si las propiedades tienen nombres distintos definir esta línea es clave para que funcione la asociación.

La definición de la entidad proveedor como opcional se usa la línea:

HasOptional(x => x.Supplier).WithMany(x => x.Products).HasForeignKey(x => x.SupplierID);

solo cambia el uso de HasOptional(), aunque sigue siendo necesario definir la propiedad Foreign Key del tipo Nullable.

 

Asociación en una única dirección


Anteriormente comente que la propiedad que permite navegar la colección de productos no es obligatoria, pudiendo definirse de esta forma:

public class Category
{
    public int CategoryID { get; set; }

    public string CategoryName { get; set; }
    public string Description { get; set; }

}

Nota: se quito la propiedad que define el ICollection<> hacia los Productos

Pero si se dejara las propiedades en la entidad Product que relacione con las entidades simples.

public class Product
{

    //resto de las propiedades

    public int CategoryID { get; set; }
    public virtual Category Category { get; set; }

    public int? SupplierID { get; set; }
    public virtual Supplier Supplier { get; set; }
}

la configuración cambiaria a:

HasRequired(x => x.Category).WithMany().HasForeignKey(x => x.CategoryID);

solo se especifica la propiedad en una única dirección hacia la entidad simple y se quitan las colecciones.

 

Repository y Lazy Load


Un punto que encontré durante la definición del repositorio se relaciona con el lazy load de las propiedades que asocia las entidades.

Si ejecutamos un test como ser

[TestMethod]
public void GetSingle_IncludeCategory_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,
        CategoryID = categoryNew.CategoryID,
        SupplierID = supplierNew.SupplierID
    };
    repoProduct.Create(productNew);

    //se recupea el producto con la categoria asociada
    var productSelected = repoProduct.Single(x => x.ProductID == productNew.ProductID,
                                            new List<Expression<Func<Product, object>>>() { x => x.Category });

    Assert.IsNotNull(productSelected.Category);
    Assert.AreEqual(productSelected.Category.CategoryID, categoryNew.CategoryID);
    Assert.AreEqual(productSelected.CategoryID, categoryNew.CategoryID);

    Assert.AreEqual(productSelected.SupplierID, supplierNew.SupplierID);
    Assert.IsNull(productSelected.Supplier);
}

El cual crea un producto con categoría y proveedor relacionado, pero luego de crear la entidad se recuperar definiendo en el “include” solo la categoría, por estar estar habilitado el lazy load se obtendrá el siguiente mensaje:

 

SNAGHTML7b064e64

Este error se produce porque se crea un proxy que permite la relación de forma desatendida, pero al estar la entidad por fuera del contexto entonces falla.

Por esta razón se definieron de dos líneas en el constructor de la clase del contexto:

this.Configuration.LazyLoadingEnabled = false;
this.Configuration.ProxyCreationEnabled = false;

Al deshabilitando lazy load las propiedades relacionadas que no se especifican en el “include” no recuperan las instancias, por lo tanto estarán en null.

Ahora si se comporta como se espera

SNAGHTML7bae9e99

 

Documentación de referencia


Code First Conventions

Configuring Relationships with the Fluent API

Working with Proxies

 

Código


Se utilizo Visual Studio 2012, no se adjunta ninguna base de datos, esta será creada al ejecutar los test utilizando el modelo definido en el contexto de EF.

[C#]