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

28 comentarios:

  1. Hola Leandro, son muy interesantes tus articulos sobre CF. Me intriga bastante el tema, estoy investigando y haciendo mis primeras pruebas, y estuve viendo que tambien se puede utilizar con BD existentes:

    http://weblogs.asp.net/scottgu/archive/2010/08/03/using-ef-code-first-with-an-existing-database.aspx
    http://weblogs.asp.net/jgalloway/archive/2011/02/24/generating-ef-code-first-model-classes-from-an-existing-database.aspx

    Me gustaria saber que opinas al respecto, estoy muy acostumbrado a trabajar primero en la BD, y luego codificar la app; mi duda es sobre todo respecto a como quedaria el mapeo si parto de una BD existente y con los tipos de herencia que se generaria a partir de la estructura de las tablas de la BD. Como me recomendarias encarar este tipo de proyecto? saludos!!

    ResponderEliminar
  2. hola matias

    puedes mapear perfectamente una db existente con clases de EF

    quizas habria que evaluar que nivel de herencia se podria aplicar, si es que se requiere, sino de ultima podrias usar la entidad simple

    la idea es no forzar un diseño que termine complicandolo todo, algunas entidades seguramente seran mas simple de llevar a un modelo de objetos, otras quizas no, y deben usarse EF simplemente para facilitar el acceso a datos y nada mas

    Code First to an Existing Database

    EntityFramework Reverse POCO Generator

    la ides es pensar la persistencia desde EF y que esta genere la db, pero sino se puede aplciar esta tecnica se trata de aproxima sin forzar

    saludos

    ResponderEliminar
  3. Hola leandro, mira estoy realizando un proyecto tipo academico en MVC4 en visual studio 2012, y no me deja conectar la base de datos del membership "defaultconConnection" por medio del asp.net , te agradeceria si me respondes
    Gracias

    ResponderEliminar
  4. hola Julian

    pero el error de conexion cual seria ?

    validaste que el connection string este bien definido, o si desde donde te ejecutas el sitio se puede alcanzar la db a la cual te quiere conectar

    saludos

    ResponderEliminar
  5. hola,
    mira, ejecuto el proyecto sin modificarle nada y no deja ingresar al membership para administrar los roles,( en mvc 4 )
    observación : se puede ingresar a la base de datos defaultconnection por el explorador de servidores y la aplicación me deja listar los usuarios registrados en el membership, pero al momento de añadir el rol dice q presenta problemas de conexión con sql (error 26 )

    saludos

    ResponderEliminar
  6. hola Julian

    pero la base de membership la tienes como un archivo .mdf en el App_Data o esta dentro del servicio de sql server

    desde donde es que agregas el rol? desde la aplicacion WAT


    saludos

    ResponderEliminar
  7. hola, la BD del membership esta configurada como .mdf
    y se guarda en app_data


    y ando agregando los roles desde el explorador de servidores

    ResponderEliminar
  8. hola Julian

    pero puedes acceder a la pagina de WAT para crear usuario y roles desde esta ?

    porque a la db que esta en App_Data no debesrias crearle registros directamente, sino por medio de la pagina que proporciona WAT

    saludos

    ResponderEliminar
  9. Hola, no se puede acceder al WAT para ingresar nuevos roles

    ResponderEliminar
  10. hola Julian

    entonces crea tus propias paginas haciendo uso de las clases de asp.net Memebership para crear los registros

    Membership Class

    veras en los metodo de la clase membership que puedes crear,eliminar, etc usuario de esas tablas

    Roles Class

    tambien esta la clase para los roles, con estas podrias crear tus propias paginas de administracion de seguridad,sin hacer uso de WAT

    ojo si usas mvc 4 quizas este configurando

    SimpleMembershipProvider Class

    esa es la que reemplaza a la de Membership

    saludos

    ResponderEliminar
  11. Hola Leandro.

    Veo interesante el sistema de Table Splitting y dividir una tabla en 2 entidades, en una podríamos poner el Id y la Descripción y en otra entidad poner el resto de propiedades. Esto viene bien a la hora de cargar un combobox, que lo cargaríamos con la entidad base.

    En los ejemplos veo cómo cargar la información extendida en una entidad base, pero mi pregunta es cómo podría hacerse para cargar todas las entidades base con su información extendida para pasarlo todo a un datagrid y poder ver toda la información.

    Gracias y felicitaciones por el blog

    ResponderEliminar
  12. hola pepito

    no se si entendi del todo el planteo, pero podrias ver de usar el Include() en la query para recuperar la info extendida

    Loading Related Entities

    saludos

    ResponderEliminar
  13. Mi pregunta era la siguiente, supongamos que tengo una entidad de tipo Ciudad dividida en 2 entidades:

    public class Ciudad {
    public int Id { get; set; }
    public string Nombre { get; set; }

    public virtual CiudadExt CiudadExt { get; set; }
    }

    public class CiudadExt {
    public int Id { get; set; }
    public int PaisId { get; set; }
    public int IdiomaId { get; set; }
    public int IntervaloId { get; set; }
    public bool Capital { get; set; }
    }

    La idea que tengo en mente es usar la clase Ciudad para cargar comboboxes, pero a la hora de gestionar las altas, modificaciones y bajas de las ciudades tener toda la información completa en un grid.

    He consultado la información que me aconsejaste y ¡¡¡ bingo !!!, ahí está la solución ¡¡¡ el include !!!

    Muchas gracias por la ayuda.

    Una cosa más, hace unos años seguí tu ejemplo de aplicación N-Tier, ¿no tendrías un ejemplo de aplicación N-Tier con Entity Framework y WPF MVVM?

    Me gustaría saber cómo quedaría el "esqueleto" de la aplicación, me refiero a cuantos proyectos habría y qué iría en cada uno de ellos, y también cómo se implementaría la navegación de Pages o UserControls usando MVVM. Se que hay frameworks como Caliburn Micro, pero me gusta aprender y saber lo que estoy haciendo.

    Si no vas a publicar ningún artículo sobre MVVM, si no es mucha molestia, podrías enviarme un ejemplo de aplicación con esas tecnologías.

    Muchas gracias y disculpa las molestias.

    Saludos.

    ResponderEliminar
  14. hola Alex

    hace tiempo tengo pensado publicar temas de mvvm pero no me hago el tiempo para poder armar algo que publicar
    los ejemplos que tengo son de la consultora donde trabajo no podrias publicarlos o compartirlo ya que tendria problemas con los clientes

    saludos

    ResponderEliminar
  15. Hola Leandro, estoy migrando un sistema en vb 2003 - al vb 2010 y tengo un problema a la hora de levantar el sistema lo cual me sale este mensaje: "se ha producido un error al crear un controlador de seccion de configuracion para uipconfiguration: el comodin '##any' permite el elemento 'navigationGraph' y hace que el modelo de contenido sea ambiguo. un modelo de contenido debe estar constituido de forma que durante la validacion de una secuencia de elementos de informacion de elemento, la particula contenida directa, indirecta o implicitamente en el mismo y con la que se debe intentar validar cada elemento de secuencia se pueda determinar de forma exclusiva sin examinar el contenido o los atributos de dicho elemento, y sin ninguna informacion acerca de los elementos del resto de la secuencia"

    si serias amable de poder ayudarme. gracias

    ResponderEliminar
  16. hola Jesus

    entiendo por el error que este se genera por culpa del archivo de configuracion del proyecto

    creas de casualidad alguna seccion custom para leer la configuracion

    usas algun componente de terceros referenciado en proyecto?

    saludos

    ResponderEliminar
  17. el sistema me lo encontre con un dll
    llamado: "Microsoft.ApplicationBlocks.UIProcess" ese es el que hace el llamado a todos los menús. Necesito de tu ayuda leandro ya que soy practicante en una empresa. gracias

    ResponderEliminar
  18. ya que ha la hora entrar al login me levanta perfectamente, y a la hora de ingresar al menu principal que lo hace con esta linea de codigo: "UIPManager.StartTask("Logon")", ya que esa linea viene hacer un metodo del dll que te lo mencione en el anterior comentario.

    ResponderEliminar
  19. hola leandro, muchisimas gracias por tu blog esta muy bueno y tambien los comentarios que realizas en el foro de microsoft, tengo una duda con vba, estoy trabajando en excel, necesito crear un arreglo de textbox, e investigado mucho y no e encontrado nada de utilidad, espero me puedas ayudar saludos y gracias

    ResponderEliminar
  20. hola azael

    no intentaste grabar una macro y despues ver el codigo que genera? eso a veces ayuda para ver que codigo vba define el excel y despues tomar la tecnica y replicarla

    saludos

    ResponderEliminar
  21. Este comentario ha sido eliminado por el autor.

    ResponderEliminar
  22. Hola leandro. En primer lugar disculpa si estoy usando mal tu blog y mi pregunta no procede aqui. En cualquier caso esta seria:
    En una arquitectura distribuida n-tiers hecha en .net, seria posible un esquema como el siguiente:
    1º En el ordenador cliente un proyecto vb.net con "winforms"
    2º En un servidor en Valencia la capa de nogocio.
    3º En un servidor en Madrid la capa modelo.
    Mi pregunta, además sería, como se comunica un winform con la capa de negocio? Podria ser con algun servicio de WCF.
    Gracias anticipadas.
    Un saludo

    ResponderEliminar
  23. Saludos Leandro, aunque mi consulta no tiene referencia con este tema, resulta que necesito cargar una imagen que se encuentra guardada en un archivo .JPG. en un webform.
    Mi código es el siguiente:

    Dim Raster As Bitmap
    Raster = New Bitmap(c:\origen\foto.jpg)

    El problema es que el archivo "foto.jpg" tiene un tamaño de 262 Mb y muestra el error "Desbordamiento".

    Cuando lo hago con imágenes mas pequeñas lo hace bien.
    Sabes cómo puedo hacer para que el programa maneje mayor cantidad de memoria para cargar imágenes grandes ??
    Gracias.

    ResponderEliminar
  24. Buenos dias Leandro Tuttini mucho gusto, no se si podrias ayudarme con la creación de una impresora virtual utilizando vb 2008 o vb 2012

    ResponderEliminar
  25. Leandro,

    Intentó con Code First que la plataforma me cree una tabla para la siguiente clase (simplificada)

    Class Forma
    public int FormaId ...
    public string Nombre ...
    public Rectangle Area ...

    Me crea la tabla pero sin el campo Area. ¿Hay alguna manera de mapear los tipos Struc como es el caso de Rectangle?

    Saludos y felicitaciones por el Blog.

    ResponderEliminar
    Respuestas
    1. hola
      La verdad es que no he usado struct para mapear campos en uan tabla, ademas este puede contener varios valores, como deberia representarlo en la tabla? lo veo dificil

      Si necesitas quita el struct o intenta mapear este a una tabla o sea que cree la tabla de nombre "Rectangle" y genere una asociacion entre la clase que estas creando y la de Rectangle

      La tabla Rectangle contendra los campos que el struct defina. Igual lo veo dificil porque se supone que al struct o class debas definirle una propiedad que actue como key

      En resumen no uses ese struct, podrias crear una propio como ser

      public class Rectangle{
      public int RectangleId {get;set;}
      public int X {get;set;}
      public int Y {get;set;}
      }

      la idea es que se genere una relacion entre las tablas

      saludos

      Eliminar
    2. Leandro gracias por tu respuesta. En realidad la idea no es crear una tabla para rectángulos, puesto que la tabla es Formas (la cuál abrevié) y Rectangle es un campo (tal vez complejo) de dicha tabla. Imagino que voy a tener que separar en cuatro campos RectX, RectY, RectW y RectH. Detrás de esto lo que hay es la información de un diseño de emisión de informes liviano que antes guardaba en XML. Abrazo!

      Eliminar
    3. hola

      El tema es que no existe de campos complejo en las base de datos, si el tipo de dato tiene varias propiedades entonces seria otra tabla que debes relacionar.

      Exacto vas a tener que llevar los 4 campos a una tabla y mapear a una entidad. Sino podrias hacer que la propiedad xml y guardarla como string, o quizas como json si es que usas una de las ultimas versiones de sql server

      saludos

      Eliminar