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#]
 

6 comentarios:

  1. Hola Leandro, Quiero pedirte disculpa por hacerte este tipo de preguntas, pero no sabria donde hacerlo y por lo tanto lo voy hacer aqui.

    Tengo una solucion con 3 proyectos en VS 2010 con C# y uno de Ellos esta Como Proyecto de Inicio y tiene un Form MID contenedor.
    Este Depende de los otros dos.

    Yo puedo enviar informacion del MDI hacia los otros Form de los 2 proyectos, hasta aqui todo normal.

    Pero mi problema es que si quisiera enviar informacion de los otros Form de los Otros proyectos hacia el MDI no sabria como hacerlo, Te Agradeceria me pudieras Colaborar con esto.


    Te Recuerdo que el form MDI se Encuentra en el proyecto de Inicio es el principal y Este Hacer Referencia a los otros dos.

    ResponderEliminar
  2. hola Manuel

    pero no puedes enviarlo en el larespuesta de los metodos que implementan estos proyectos ?

    o sea si en el proyecto 2 tienes un metodo que sea

    public DataTable ObtenerClients(){
    //aqui codigo
    }

    ese metodo devuelve datos a quien lo invoque desde el proyecto WinForm

    sino la otra es usar eventos, desde el proyecto WinForm te adjuntas a eventos que defines en los otros proyectos y asi enviar datos sin que el primer proyecto los solicite

    el tema es que el control siempre los tiene el proyecto principal, no puede hacerlo los otros dos ya que solo son referenciados

    saludos

    ResponderEliminar
  3. Estimado leandro, en el caso de que una entidad se relacione de dos maneras diferentes con otra entidad? ambas obligatorias y ambas 1-N.?

    ResponderEliminar
  4. hola El Gran Capitan

    podrias crear una clase base para que se relacione con esa entidad

    entonces implementas por herencia las entidades con las cual se relaciona

    basicamente implementas herencia y la relacionas la entidad con esa clase base

    saludos

    ResponderEliminar
  5. Leandro, en que casos puede ser HasOptional, si es un ForenKey es por que se quiere integridad referendial de datos.

    ResponderEliminar
    Respuestas
    1. hola
      Una relacion es opcional cuando el campo que define la relacion permite nulos.

      saludos

      Eliminar