lunes, 9 de septiembre de 2013

[Entity Framework][Code First] Dividir Tabla (Table Splitting)

 

Introducción


Cuando se definen entidades estas pueden contener atributos que no siempre se quiere recuperar, ya que de hacerlo podría afectar la performance. Un ejemplo se observa en aquellos atributos donde se persisten datos de un volumen importante, como ser campos del tipo image, varbinary o ntext, los cuales por lo general no son necesarios recuperar cuando se carga un listado de la entidad en un grid, pero si son necesarios cuando se edita una entidad individual.

Por supuesto no solo esta limitado a este uso, se podría dividir simplemente porque un conjunto de propiedades representan la información clave de la entidad la cual es frecuentemente utilizada, separándola de aquellos atributos donde, salvo en algún uso puntual, no siempre se requieren.

Esta división permite mapear una tabla simple en múltiples entidades.

 

Complex Type Vs Table Splitting

Seguramente se estén preguntando, porque simplemente no se usa un Tipo Complejo? definiendo en este las propiedades que se consideran especiales.

El tema pasa porque los Tipo Complejos no permiten la carga retardada (lazy load), mientras que la división de la tabla en varias entidades si lo permite.

Al recuperar una entidad los tipo complejo serán siempre devueltos, no hay control sobre esta entidad, mientras que una tabla dividida en entidades se le puede indicar cuando se necesita recuperar este bloque de datos.

 

Definición del modelo


El objetivo es permitir separar un conjunto de propiedades en una entidad distinta

image

a pesar de esta dividido en varias entidades todas son mapeadas a la misma tabla, por lo que resulta

image

 

public class Employee
{
    public Employee()
    {
        this.Localization = new Localization();
    }

    public int EmployeeID { get; set; }

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

    public virtual Localization Localization { get; set; }

    public virtual EmployeeExtended EmployeeExt { get; set; }

}

public class EmployeeExtended
{
    public int EmployeeID { get; set; }

    public byte[] Photo { get; set; }
    public string PhotoPath { get; set; }

    public string Notes { get; set; }
    
}

La entidad principal define una propiedad que referencia a la entidad extendida, a la vez que ambas entidades definen la misma propiedad como key, ya que por medio de esta se relacionan.

 

Definición del Mapping


Al definir el mapping ambas entidades persisten en la misma tabla, por eso el uso del ToTable(). También hacen uso de la misma key.

El HasRequired() permite indicar cual será la propiedad que representaran los datos extendidos en la entidad principal.

 

public class EmployeeMap : EntityTypeConfiguration<Employee>
{
    public EmployeeMap()
    {
        ToTable("Employees");
        HasKey(x => x.EmployeeID);

        Property(x => x.LastName)
            .HasMaxLength(20)
            .IsRequired();

        Property(x => x.FirstName)
            .HasMaxLength(10)
            .IsRequired();

        HasRequired(x => x.EmployeeExt)
                    .WithRequiredPrincipal();
    }
}

public class EmployeeExtendedMap : EntityTypeConfiguration<EmployeeExtended>
{
    public EmployeeExtendedMap()
    {
        ToTable("Employees");
        HasKey(x => x.EmployeeID);

        Property(x => x.Notes)
            .HasColumnType("ntext")
            .IsOptional();

        Property(x => x.Photo)
            .HasColumnType("image")
            .IsOptional();

        Property(x => x.PhotoPath)
            .HasColumnType("nvarchar")
            .HasMaxLength(255)
            .IsOptional();

    }
}

Definición del Repositorio


Hay cierta funcionalidad que requiere un tratamiento especial para estos casos es que se crea funcionalidad en el repositorio de le entidad empleado.

Se define esta en la interfaz, para luego implementarla en la clase concreta.

 

public interface IEmployeeRepository : IRepository<Employee>
{
    EmployeeExtended GetExtendedById(int id);

    void DeleteIncludeExtended(Employee entity);
}

 

public class EmployeeRepository : BaseRepository<Employee>, IEmployeeRepository
{
    /// <summary>
    /// Recupera solo la entidad Extendida
    /// </summary>
    /// <param name="id"></param>
    /// <returns></returns>
    public EmployeeExtended GetExtendedById(int id)
    {
        using (NorthWindContext context = new NorthWindContext())
        {
            return context.EmployeeExtendeds.FirstOrDefault(x => x.EmployeeID == id);
        }
    }

    /// <summary>
    /// Elimina la entidad incluyendo la informacion extendida 
    /// en caso de tenerla
    /// </summary>
    /// <param name="entity"></param>
    public void DeleteIncludeExtended(Employee entity)
    {
        using (NorthWindContext context = new NorthWindContext())
        {

            if (entity.EmployeeExt == null)
                entity.EmployeeExt = new EmployeeExtended() { EmployeeID = entity.EmployeeID };

            context.Employees.Attach(entity);
            context.Employees.Remove(entity);

            context.SaveChanges();

        }
    }

}

Estos método serán utilizados en los Test, donde se probara su funcionalidad

 

Configuración del proyecto de Test


Para probar la funcionalidad se requiere que un archivo que representa la foto que será asignada a la propiedad de la entidad. Este archivo forma parte del proyecto de Test, pero debe ser copiado a la carpeta de output.

Sera necesario definir un archivo de TestSetting el cual no ayudara a configurar el entorno de prueba, para ello agregamos este archivo usando la opción

image

 

SNAGHTML4800a637

En este configuramos las opciones de deploy para que copie el archivo de la imagen a la carpeta que defina el test cuando ejecute

SNAGHTML480139af

Asignamos cual será el archivo usado en la ejecución de los test

image

 

SNAGHTML47f6aaaa

 

Test – Eliminar empleado con y sin información extendida


Si eliminamos una entidad haciendo uso del método Delete() del repositorio base obtendremos un error

image

Se produce porque se requiere no solo marcar para eliminar la entidad principal sino también la extendida, por esta razón en el repositorio de la entidad empleado se creo un método especifico para este caso.

El método DeleteIncludeExtended() elimina la entidad y también define la entidad extendida a pesar que no se envíe como dato en la entidad que se asigna como parámetro.

 

[TestMethod]
public void Delete_Employee()
{
    EmployeeRepository repoEmployee = new EmployeeRepository();

    //
    //se crea un empleado con info extendida
    //
    CreateEmployeeWithPhoto();

    //elimina el empleado y su informacion extendida
    repoEmployee.DeleteIncludeExtended(new Employee() { EmployeeID = employeeNew.EmployeeID } );

    //se recupera el empleado que fue eliminado en el paso anterior
    Employee employeeSel = repoEmployee.Single(x => x.EmployeeID == employeeNew.EmployeeID);

    Assert.IsNull(employeeSel);


    //
    //se crean el empleados sin informacion extendida
    //
    Employee employee = new Employee()
    {
        FirstName = "name 1",
        LastName = "lastname 1",
        EmployeeExt = new EmployeeExtended()
    };
    repoEmployee.Create(employee);

    repoEmployee.DeleteIncludeExtended(employee);

    //se recupera el empleado que fue eliminado en el paso anterior
    employeeSel = repoEmployee.Single(x => x.EmployeeID == employee.EmployeeID);

    Assert.IsNull(employeeSel);

}

El test tiene dos partes, la primera crea un empleado con funcionalidad extendida y lo elimina usando el método que hemos creado en el repositorio para este propósito, la segunda parte crea una entidad sin información extendida y también elimina esta entidad, demostrando que sin importar la información con la cual se cree la entidad puede ser eliminada.

Como se vera para ambos casos es necesario definir el id de la entidad extendida para que sea marcada tanto la entidad principal como la extendida para ser eliminadas.

 

Test – Obtener todos los empleados


Se crean dos empleado, uno con información extendida y el otro sin ella.

En el test recupera la lista de empleados, pudiéndose analizarse como la información extendida no se obtiene, mientas que la propiedad que representa el complex type si es devuelta al recuperar la entidad.

Este caso es el típico que uno usaría para cargar un listado o grid con la información base de la entidad.

 

[TestMethod]
public void GetAll_Employee()
{
    string foto = Path.Combine(this.TestContext.DeploymentDirectory, "foto.jpg");

    EmployeeRepository repoEmployee = new EmployeeRepository();

    //se eliminan las entidades que pudieran quedar de la ejecucion de test anteriores
    repoEmployee.GetAll().ForEach(x => repoEmployee.DeleteIncludeExtended(x));


    //se crean dos empleados
    CreateEmployeeWithPhoto();

    Employee employee2 = new Employee()
    {
        FirstName = "name 2",
        LastName = "lastname 2",
        EmployeeExt = new EmployeeExtended()
    };
    repoEmployee.Create(employee2);


    List<Employee> list = repoEmployee.GetAll();

    Assert.AreEqual(list.Count, 2);

    Assert.IsNull(list[0].EmployeeExt);
    Assert.IsNotNull(list[0].Localization);
    Assert.IsTrue(list[0].Localization.HasValue);

    Assert.IsNull(list[1].EmployeeExt);
    Assert.IsNotNull(list[1].Localization);
}

Test – Obtener entidad SIN información extendida


Aunque se haya creado un empleado con información adicional al recuperar la entidad esta no forma parte de los datos.

 

[TestMethod]
public void GetById_Employee()
{
    //se crea el empleado con informacion extendida
    CreateEmployeeWithPhoto();


    //Se recupera la entidad
    Employee employeeSel = repoEmployee.Single(x => x.EmployeeID == employeeNew.EmployeeID);


    Assert.AreEqual(employeeSel.EmployeeID, employeeNew.EmployeeID);
    Assert.IsTrue(employeeSel.Localization.HasValue);
    Assert.AreEqual(employeeSel.Localization.Address, "Address 1");
    Assert.AreEqual(employeeSel.Localization.City, "City 1");
    Assert.AreEqual(employeeSel.Localization.Country, "Country 1");

    Assert.IsNull(employeeSel.EmployeeExt);

}

La consulta recupera el empleado y deja evidencia de la inclusión de los campos que define el complex type pero no se incluyen los de la entidad extendida.

image

 

Test – Obtener entidad CON información extendida


En este caso el test define un “include” de la propiedad extendida, permitiendo recuperar la foto asignada al empleado

 

[TestMethod]
public void GetById_EmployeeExt()
{
    //se crea el empleado con informacion extendida
    CreateEmployeeWithPhoto();

    //se recupera la entidad y sus propiedad extendida
    Employee employeeSel = repoEmployee.Single(x => x.EmployeeID == employeeNew.EmployeeID,
                                                    new List<Expression<Func<Employee, object>>>() 
                                                    { 
                                                        x=>x.EmployeeExt 
                                                    });
                        


    Assert.AreEqual(employeeSel.EmployeeID, employeeNew.EmployeeID);
    Assert.IsTrue(employeeSel.Localization.HasValue);
    Assert.AreEqual(employeeSel.Localization.Address, "Address 1");
    Assert.AreEqual(employeeSel.Localization.City, "City 1");
    Assert.AreEqual(employeeSel.Localization.Country, "Country 1");

    Assert.IsNotNull(employeeSel.EmployeeExt);
    Assert.IsNotNull(employeeSel.EmployeeExt.Photo);
    Assert.AreEqual(employeeSel.EmployeeExt.PhotoPath, employeeNew.EmployeeExt.PhotoPath);

}

la consulta resultante es bastante mas compleja ya que debe unir los campos de la entidad extendida

image

Lo importante a destacar es que uno controla cuando es necesario recuperar los datos extendido para así mejorar la performance de la aplicación.

 

Test – Recuperar SOLO la información extendida


Si ya se dispone de la entidad con los datos básicos y se requiere solo los datos extendidos se podría recuperar únicamente estos.

 

[TestMethod]
public void GetById_Employee_RecoverOnlyExtended()
{

    CreateEmployeeWithPhoto();


    EmployeeExtended employeeSel = repoEmployee.GetExtendedById(employeeNew.EmployeeID);


    Assert.IsNotNull(employeeSel);
    Assert.AreEqual(employeeSel.EmployeeID, employeeNew.EmployeeID);
    Assert.IsNotNull(employeeSel.Photo);
    Assert.AreEqual(employeeSel.PhotoPath, employeeNew.EmployeeExt.PhotoPath);

}

El método GetExtendedById() hace uso de la propiedad del contexto que accede a esta entidad extendida de forma directa.

image

Es la misma consulta que recupera al empleado pero solo define los campos de la entidad extendida.

 

Documentación de referencia


 Associations in EF Code First: Part 4 – Table Splitting

 

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