lunes, 26 de agosto de 2013

[Entity Framework][Code First] Herencia - Tabla por jerarquía - Table per Hierarchy (TPH)

 

Introducción


Una de las principales ventajas al implementar un ORM, en nuestro caso de la mano de Entity Framework, apunta a tener a nuestra disposición todo el poder de la Programación Orientada a Objeto (POO) para modelar nuestras entidades de negocio.

La Herencia es una de las practicas mas utilizadas para modelar entidades, pudiendo representar en un modo realista el diseño del negocio, mejor aun si le unimos un fácil mapeo de las entidades con la estructura de la base de datos.

Existen tres formas de mapear una estructura de Herencia con tablas:

  1. Tabla por jerarquía - Table per Hierarchy (TPH)
  2. Tabla por tipo - Table per Type (TPT)
  3. Tabla por tipo concreto - Table per Concrete Type (TPC)

 

En este articulo tratare el primero de ellos, Tabla por jerarquía, en este las diferentes clases que definen la herencia mapean contra una única tabla en la base de datos utilizando un campo discriminador para determinar el tipo especifico.

 

Definición del modelo


En el ejemplo definiremos una entidad Empleado pudiendo encontrarse dos tipo: los empleados internos de la empresa y los de contratación externa.

 

public abstract 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 class EmployeeExternal : Employee
{
    public string ConsultantName { get; set; }

    public DateTime? ContactExpiration { get; set; }
}

public class EmployeeInternal : Employee
{
    public DateTime? HireDate { get; set; }
}

 

En al definición de las clases pueden observarse dos detalles:

  1. la clase base se define como abstract, este impedirá crear instancias del tipo base, con lo cual se obliga a crear instancias solo de los tipos derivados.
  2. las clases hijas poseen propiedades concretas para cada tipo que definen características determinadas, es recomendable que las propiedades de las clases derivadas permitan nulos o sino asignarle un valor por default.

La idea es poder mapear el modelo de objetos como el siguiente

image

generando una tabla como ser

image

 

Definición del Mapping


Definir como se debe persistir este tipo de modelo es bastante simple y no difiere a lo ya aprendido en los anteriores artículos que realice sobre el tema.

Se define el contexto de EF

public class NorthWindContext : DbContext
{

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

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

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

        base.OnModelCreating(modelBuilder);
    }

}

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

        Map<EmployeeInternal>(x => x.Requires("Type")
                                        .HasValue("I")
                                        .HasColumnType("char")
                                        .HasMaxLength(1));

        Map<EmployeeExternal>(x => x.Requires("Type")
                                        .HasValue("E"));

    }
}

 

Para la definición de la  herencia se debe puntualizar estas líneas:

image

En ellas se define el nombre del campo que actuara como discriminador del tipo, así como los valores que tomara para cada clase hija definida, opcionalmente se puede especificar el tipo y precisión de la columna.

Sino se especifica el tipo para la columna del discriminador Entity Framework usara valores por defecto, por lo que la columna podrías crearse como nvarchar(128), esto puede resultar de poca importancia, pero si solo se va a contener un único carácter se estaría desperdiciando espacio para ese campo.

El campo definido como discriminador no se define como propiedad en las clases de la entidad de dominio, ya que la propia instancia de la clase define el tipo en si mismo.

 

Definición Repository


La definición del repositorio tiene algunas novedades respecto a los artículos anteriores, en este caso junto al proyecto que de entidades se define la interfaz para poder extender el repositorio.

Quizás en este momento no se aprecie este tipo de implementación, pero si se hace uso de algún framework de IoC (Invertion of Control) como ser Ninject, Unity, etc, allí si se requieren interfaces para poder desacoplar la creación del repositorio concreto.

 

public interface IEmployeeRepository : IRepository<Employee>
{
    List<EmployeeInternal> GetAllInternalType();
}

 

public class EmployeeRepository : BaseRepository<Employee>, IEmployeeRepository
{
    /// <summary>
    /// Retorna todos los empleados externos a la empresa
    /// </summary>
    /// <returns></returns>
    public List<EmployeeInternal> GetAllInternalType()
    {
        using (NorthWindContext context = new NorthWindContext())
        {
            return context.Employees.OfType<EmployeeInternal>().ToList();
        }
    }

}

public class EmployeeInternalRepository : BaseRepository<EmployeeInternal>
{

}

public class EmployeeExternalRepository : BaseRepository<EmployeeExternal>
{

}

Test – Inicializar datos


En si mismo la inicialización de los datos no son un test, pero como todos los test harán uso de un mismo conjunto de datos se podría decir que la inicialización también aplica pruebas en al creación de las entidades.

La ejecución de esta inicialización implica la validación de los métodos de creación de las entidades.

 

private void InitializeTestData()
{
    IEmployeeRepository repoEmployee = new EmployeeRepository();
    
    //
    // elimino registros previos
    //
    List<Employee> list = repoEmployee.GetAll();

    list.ForEach(x => repoEmployee.Delete(x));


    //
    // creo un empleado interno
    //
    employee1 = new EmployeeInternal()
    {
        FirstName = "name1",
        LastName = "lastname1",
        HireDate = DateTime.Now.AddMonths(-10)
    };
    repoEmployee.Create(employee1);

    //
    // creo un empleado externo
    //
    employee2 = new EmployeeExternal()
    {
        FirstName = "name2",
        LastName = "lastname2",
        ConsultantName = "ConsultantName2",
        ContactExpiration = DateTime.Now.AddYears(2)
    };
    repoEmployee.Create(employee2);

    //
    // creo otro empleado externo
    //
    employee3 = new EmployeeExternal()
    {
        FirstName = "name3",
        LastName = "lastname3",
        ConsultantName = "ConsultantName3",
        ContactExpiration = DateTime.Now.AddYears(1)
    };
    repoEmployee.Create(employee3);
}

La inicialización de los datos ejecuta instrucciones sql donde se pueden observar el campo definido como discriminador

image

 

Test – Obtener todos los empleados


Obtendremos la lista de todos los empleados pudiendo validar el tipo de cada uno de ellos.

 

[TestMethod]
public void GetAll_Employee()
{
    InitializeTestData();

    //
    //recupero todos los empleados
    //
    IEmployeeRepository repoEmployee = new EmployeeRepository();

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

    //
    // Assert
    //
    Assert.AreEqual(listIntEmployee.Count, 3);

    Assert.IsInstanceOfType(listIntEmployee[0], typeof(EmployeeInternal));
    Assert.IsInstanceOfType(listIntEmployee[1], typeof(EmployeeExternal));
    Assert.IsInstanceOfType(listIntEmployee[2], typeof(EmployeeExternal));

}

image

El query creado para recuperar todos los empleados incluye un filtro que especifica todos los tipos existentes.

 

Test – Recuperar todos los empleados de tipo interno, usando un repositorio especifico


Se recuperan las entidades que corresponden a empleados propios de la empresa, pero para lograrlo se hace uso del repositorio definido para ese tipo concreto.

Se define un repositorio concreto para la clase EmployeeInternal, pudiendo utilizar los métodos que define el RepositorioBase<>

 

[TestMethod]
public void GetAllInternal_UsingSpecificRepository_Employee()
{
    InitializeTestData();

    //
    //recupero solo empleados internos
    //
    IRepository<EmployeeInternal> repoInternalEmployee = new EmployeeInternalRepository();

    List<EmployeeInternal> listIntEmployee = repoInternalEmployee.GetAll();

    //
    // Assert
    //
    Assert.AreEqual(listIntEmployee.Count, 1);

    Assert.IsInstanceOfType(listIntEmployee[0], typeof(EmployeeInternal));

    Assert.AreEqual(listIntEmployee[0].FirstName, employee1.FirstName);
    Assert.IsNotNull(listIntEmployee[0].HireDate);
    Assert.AreEqual(listIntEmployee[0].HireDate.Value.ToShortDateString(), employee1.HireDate.Value.ToShortDateString());

}

image

Al recuperar todas las instancia para un tipo en concreto la query filtra por el identificado definido para ese tipo.

Seguramente se preguntaran que significa el

'0X0X' AS [C1],

esa línea es usada internamente por Entity Framework para saber la instancia de que tipo en concreto tiene que materializar, o sea es una marca que define el tipo.

Test – Recuperar todos los empleados del tipo interno, usando funcionalidad del repositorio genérico


Se recuperan las entidades que corresponden a empleados propios de la empresa, pero en este caso se utilizara el repositorio definido para la clase base.

Es por medio del OfType<> que se especifica que tipo concreto se quiere recuperar.

 

[TestMethod]
public void GetAllInternal_UsingGenericRepository_Employee()
{

    InitializeTestData();

    //
    //recupero solo empleados internos
    //
    IEmployeeRepository repoEmployee = new EmployeeRepository();

    List<EmployeeInternal> listIntEmployee = repoEmployee.GetAllInternalType();

    //
    // Assert
    //
    Assert.AreEqual(listIntEmployee.Count, 1);

    Assert.IsInstanceOfType(listIntEmployee[0], typeof(EmployeeInternal));

    Assert.AreEqual(listIntEmployee[0].FirstName, employee1.FirstName);
    Assert.IsNotNull(listIntEmployee[0].HireDate);
    Assert.AreEqual(listIntEmployee[0].HireDate.Value.ToShortDateString(), employee1.HireDate.Value.ToShortDateString());

}

image

Usar el OfType<> genera el mismo resultado que especializar el repositorio, la query generada son idénticas

 

Test – Obtener todos los empleados Externos


Se obtienes todos los empleados de contratación externa.

 

[TestMethod]
public void GetAllExternal_Employee()
{

    InitializeTestData();

    //
    //recupero solo empleados externos
    //
    IRepository<EmployeeExternal> repoExternalEmployee = new EmployeeExternalRepository();

    List<EmployeeExternal> listExtEmployee = repoExternalEmployee.GetAll();

    //
    // Assert
    //
    Assert.AreEqual(listExtEmployee.Count, 2);

    Assert.IsInstanceOfType(listExtEmployee[0], typeof(EmployeeExternal));
    Assert.IsInstanceOfType(listExtEmployee[1], typeof(EmployeeExternal));

    Assert.AreEqual(listExtEmployee[0].FirstName, employee2.FirstName);
    Assert.IsNotNull(listExtEmployee[0].ContactExpiration);
    Assert.AreEqual(listExtEmployee[0].ContactExpiration.Value.ToShortDateString(), employee2.ContactExpiration.Value.ToShortDateString());
    Assert.AreEqual(listExtEmployee[0].ConsultantName, employee2.ConsultantName);

    Assert.AreEqual(listExtEmployee[1].FirstName, employee3.FirstName);
    Assert.IsNotNull(listExtEmployee[1].ContactExpiration);
    Assert.AreEqual(listExtEmployee[1].ContactExpiration.Value.ToShortDateString(), employee3.ContactExpiration.Value.ToShortDateString());
    Assert.AreEqual(listExtEmployee[1].ConsultantName, employee3.ConsultantName);
}

image

 

 

Poder crear instancias de la clase base


Al comienzo del artículo comente que la clase base se define con abstract para así forzar siempre usar las derivadas, al generar la tabla el campo que actúa como discriminador no permita nulo.

Ahora si queremos crear instancias de la clase base, solo será cuestión de permitirlo quitando el abstract.

 

image

la tabla generada por EF ahora permite nulo en el campo “Type”

image

con lo cual se podrán crear instancias del tipo base “Employee”, para probarlo creamos un test

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

    //
    // creo un empleado interno
    //
    EmployeeInternal employee1 = new EmployeeInternal()
    {
        FirstName = "name1",
        LastName = "lastname1",
        HireDate = DateTime.Now.AddMonths(-10)
    };
    repoEmployee.Create(employee1);

    //
    // creo un empleado interno
    //
    Employee employee2 = new Employee()
    {
        FirstName = "name2",
        LastName = "lastname2"
    };
    repoEmployee.Create(employee2);

    //
    //recupero todos los empleados
    //

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

    //
    // Assert
    //
    Assert.AreEqual(listIntEmployee.Count, 2);

    Assert.IsInstanceOfType(listIntEmployee[0], typeof(EmployeeInternal));
    //validamos los tipos base de cada objeto recuperado
    Assert.AreEqual(listIntEmployee[0].GetType().BaseType, typeof(Employee));
    Assert.AreEqual(listIntEmployee[1].GetType().BaseType, typeof(object));

}

Al final del test se valida los tipo base de cada instancia, para el empleado interno será la clase “Employee”, pero para una instancia base del empleado al no derivar de ninguna otra será el tipo “object”.

El query generado en este caso es bastante mas complejo

image

por eso de ser posible definir la herencia para usar solo las clases hijas

 

Campos discriminador numérico


Además de definir un campo discriminador del tipo string o char, también se puede definir numérico, solo hay que especificar los valores en el mapping

image

la creación de la tabla cambiara el campo “Type” como numérico

image

 

Documentación de referencia


Inheritance with EF Code First: Part 1 – Table per Hierarchy (TPH)

 

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

15 comentarios:

  1. Hola leandro me parece muy interesenta tu esplicacion de los entity. pero tengo un apequeña pregunta..

    Es con respecto a encriptacmiento, vera tengo este procedmiento

    public static string leerContraseña(string input)
    {

    MD5CryptoServiceProvider md5Hasher = new MD5CryptoServiceProvider();
    byte[] data = md5Hasher.ComputeHash(Encoding.Default.GetBytes(input));
    StringBuilder sBuilder = new StringBuilder();
    int i = 0;
    for (i = 0; i <= data.Length - 1; i++)
    {
    sBuilder.Append(data[i].ToString("x2"));
    }
    return sBuilder.ToString().ToUpper();

    }

    este funciona perfecto, pero solo me sirve para encriptar, ahora quisiera saver como se desencriptaria esto?
    Algun Ejemplo.

    Gracias y perdone la pregunta..

    ResponderEliminar
  2. hola Marcos

    el algoritmo MD5 aplica un hash sobre el texto por lo cual encripta en un solo sentido, no se pude recuperar una vez encriptado

    la idea es aplicar el hash al texto original y guardarlo

    luego cuando alguien ingresa una contraseña aplicas el hash sobre ese input y comparas contra el que tenias en la db, si son iguales entonces pasa la validaciones

    ante un misma entrada el algoritmo hash obtienes el mismo resultado, por eso es que no se requiere desencriptar, sino que siempre encriptas y comparas

    es ideal para armar un login de usuario, yo lo implemento aqui:

    Login – Usando Password con Hash

    saludos

    ResponderEliminar
  3. hola sr. Leandro le escribo porque ha respondido algunas de mis preguntas en el foro de microsoft, ahorita me encuentro con un problema que no he podido solucionar.

    Tengo el programa que esta a continuacion, y cuando entro en el primer thread llamado (controlobservador) tengo un problema ante las mismas condiciones la ecuacion Xac[2] calcula diferentes valores, es decir tengo la ecuacion

    Xac[2] = (X) - (To * 0.007230 * (X)) + (To * 0.812 * Ref) - (To * 0.1267 * (Xo[0] - 14.6474));

    para los mismos valores de X y Xo[0] en diferentes corridas calcula distintos valores, porque? hay alguna libreria que no estoy usando? parece que realiza mal el calculo matematico, otra cosa es q al final del hilo yo redondeo el valor medido Xo[0], sera que no lo esta leyendo redondeado?.. estoy observando los valores paso a paso gracias a que guardo toda la data en un .txt, de hecho esos valores los agarro y los uso en matlab y matlab calcula como la calculadora pero aca en el visual no...

    Que algoritmo esta utilizando visual para ejecutar esta ecuacion?

    porfavor agradeceria su ayuda porque estoy teniendo discrepancia en mis resultados y no entiendo porq el visual esta calculando mal...

    me gustaria enseñarle todo el codigo pero por aca no puedo.. porq es muy largo en el foro ya habia formulado la pregunta en este link... apreciaria su ayuda

    http://social.msdn.microsoft.com/Forums/es-ES/341ed1b5-8fa5-4c17-978c-bd0d9c6e8a69/c?forum=vcses

    ResponderEliminar
  4. hola Mariett

    respondi en el foro

    saludos

    ResponderEliminar
  5. Gracias por tu ayuda, ya he arreglado el problema. pero me encuentro con otro problema a partir del codigo que muestro en el foro, parece que los puertos de la tarjeta de adquisicion que utilizo no se limpian en cada corrida, quisiera hacer un thread que envie valores aleatorios del 0 al 5, cada 0.1 seg.
    tienes alguna idea de como podria hacer eso?

    ResponderEliminar
  6. igual postie mi pregunta mas especifica en el foro...

    http://social.msdn.microsoft.com/Forums/es-ES/654f6d0a-df1b-4db6-8713-a292b5fe409d/un-thread-con-numeros-aleatorios?forum=webdeves


    y muchas gracias por tu ayuda...

    ResponderEliminar
  7. hola Mariett

    respondi en el foro

    saludos

    ResponderEliminar
  8. hola leandro en principio quiero felicitarte por tu blog y queria peguntarte como puedo mostrar toda una tabla de una base de datos mysql en texboxes, o sea en lugar de usar un datagridview quiero usar una grilla pero de texboxes, saludos, Yamil

    ResponderEliminar
  9. hola david

    pues la verdad no se me ocurre como se podria lograr y que quede bien

    quizas podrias armar lineas de string que despues muestre en un textbox definido como multiline

    o quizas usar el control RichTextBox

    en este control mostrarias el string en donde cada linea seria un registro y cada valo lo separas por algun caracter o espacio

    saludos

    ResponderEliminar
  10. Estimado Leandro, que paso por que ya no publicas nuevos artículos, tus artículos siempre fueron lo mejor para los que nos gusta aprender sobre .net, la verdad me gustaria leer nuevos artículos tuyos, por jemplo de asp.net 5, azure, no se cosas innovadores que a la mano tuya serian mas faciles de aprender. Saludos desde Bolivia.

    ResponderEliminar
    Respuestas
    1. hola
      Si la verdad es que he descuidado el blog, este ultimo tiempo se me hizo dificil dedicarme a crear nuevos articulos, por eso me dedique mas a los foros. Voy a ver que material puedo ir armando.
      saludos

      Eliminar
    2. Hola Leandro, me sumo al pedido, y estaría bueno que escribas sobre arquitectura del SW y buenas prácticas: Patrones de diseño, clean code, S.O.L.I.D, TDD...
      Creo que somos muchos los que programamos pero no tenemos bien claro estos temas, y no hay demasiado publicado, sobre todo en el mundo de .net.

      Saludos!

      Eliminar
  11. Hola Leandro, tengo una duda respecto a ENTITY FRAMEWORK. Hice una web api y generè el modelo mediante DATABASE FIRST. Cuando reviso las propiedades de los campos, veo que en la base de datos hay DEFAULT, pero no se generaron cuando se creo el modelo. Quisiera saber si hay un motivo de fondo; ya se que puedo modificar cada campo manualmente, pero si tengo muchas tablas, es muy trabajoso y con riesgo a equivocarme. Agredezco tu respuesta y toda informaciòn acerca del tema. saludos

    ResponderEliminar