jueves, 14 de marzo de 2013

n-Layer - SchoolManager - Herencia y navegación de entidades relacionadas (1/2)

 

Introducción


El diseño de una estructura en capas mucha veces requiere que se trabajen con objetos complejos, por lo general una entidad no tienes solo propiedades simples, algunas puedes representar la relación con otras entidades, es mas puede que la entidad en si sea solo una parte de una mayor

En esta oportunidad nos centraremos justamente en dos aspectos:

  • representar una herencia, aquí abordaremos no solo como recuperar una entidad definida con un padre, sino también como persistirla
  • cargar entidades relacionadas,

Para esto contaremos con la entidad Instructor en el modelo de administración de la base de datos de una escuela.

SNAGHTML248b611

El modelo de datos que usaremos define la tabla Persona, pero en la misma tabla se pueden abstraer otras dos entidades Instructor y Alumno.

image

La implementación de la herencia en este caso lleva el nombre de “tabla por subclase”, en donde la relación uno a uno con la tabla OfficeAssigment determina si es un instructor o un Alumno, en este caso no se usa ningún campo discriminador para el tipo, la relación actúa como medio para determinarlo. Si hay una relación con la tabla OfficeAssigment  será un instructor, sino lo hay será un alumno.

 

Herencia de entidades (Recuperar entidad) 


Las entidades intervinientes en este modelo se representan en la siguiente imagen:

image

 

A simple vista se puede observar que la entidad Instructor hereda de persona, la pregunta que trataremos de responder es como definir un modelo de persistencia para esta entidad.

Empezaremos analizando la clase InstructorRepository la cual cuenta con el método:

public static InstructorEntity GetByKey(int id)
{
    InstructorEntity item = null;

    using (SqlConnection conn = new SqlConnection(ConfigurationManager.ConnectionStrings["default"].ToString()))
    {
        conn.Open();

        string query = @"SELECT P.PersonID, 
                                P.LastName, 
                                P.FirstName, 
                                P.HireDate, 
                                P.EnrollmentDate, 
                                OA.Location
                        FROM Person P 
                            INNER JOIN OfficeAssignment OA 
                            ON P.PersonID = OA.InstructorID
                        WHERE P.PersonID = @id
                        ORDER BY P.PersonID";

        SqlCommand cmd = new SqlCommand(query, conn);
        cmd.Parameters.Add("@id", SqlDbType.Int).Value = id;

        SqlDataReader reader = cmd.ExecuteReader(CommandBehavior.CloseConnection);

        if (reader.Read())
        {
            item = ConvertInstructor(reader);
        }

    }

    return item;
}
private static InstructorEntity ConvertInstructor(IDataReader reader)
{
    InstructorEntity item = new InstructorEntity();

    item.PersonID = Convert.ToInt32(reader["PersonID"]);
    item.LastName = Convert.ToString(reader["LastName"]);
    item.FirstName = Convert.ToString(reader["FirstName"]);

    item.HireDate = reader["HireDate"] == DBNull.Value ? (DateTime?)null : Convert.ToDateTime(reader["HireDate"]);
    
    item.Location = Convert.ToString(reader["Location"]);

    return item;


}

Recuperar una entidad, o una lista de Instructores no parece diferir mucho a como se haría con una entidad simple, en este caso la query involucra tanto la tabla del Instructores como la de Personas, lo que implica usar el INNER JOIN para unir los registros.

Herencia de entidades (Crear entidad)


Donde si veremos mayor cambios es al momento de actualizar la entidad, pues requiere impactar las actualizaciones en dos tablas diferentes

Empecemos por crear un nuevo instructor, la clase InstructorRepository contiene el método Save()

Definir un instructor implica varios pasos:

  1. crear el registro en la tabla base, en este caso insertar el registro en Persona
  2. crear el registro en la tabla OfficeAssignment
  3. si la entidad tenia cursos asignados se crea la relación con esto

 

1- Grabar la entidad Persona, esta operación es bien simple, solo implica un INSERT en la tabla y recuperar el id generado.

public static void Save(PersonEntity person)
{
    string sql = @"INSERT INTO Person (
                    LastName,
                    FirstName)
              VALUES (@LastName, 
                    @FirstName);
              SELECT SCOPE_IDENTITY";


    using (SqlConnection conn = new SqlConnection(ConfigurationManager.ConnectionStrings["default"].ToString()))
    {
        conn.Open();

        SqlCommand cmd = new SqlCommand(sql, conn);
        cmd.Parameters.AddWithValue("@LastName", person.LastName);
        cmd.Parameters.AddWithValue("@FirstName", person.FirstName);

        person.PersonID = Convert.ToInt32(cmd.ExecuteScalar());

    }
}

2- Aquí no solo se actualiza los datos concretos del instructor en la tabla padre, sino que además se inserta en la tabla concreta que define el tipo, en esta operación se hace uso del mismo id que se recupero al crear la entidad padre.

public static void Save(InstructorEntity instructor)
{

    PersonRepository.Save((PersonEntity)instructor);

    using (SqlConnection conn = new SqlConnection(ConfigurationManager.ConnectionStrings["default"].ToString()))
    {
        conn.Open();

        //
        // Actualiza los campos de la tabla Persona 
        // con el campo que define solo el instructor
        //
        string sqlUpdateP = @"UPDATE Person 
                            SET HireDate = @HireDate 
                            WHERE PersonID = @PersonID";

        using (SqlCommand cmd = new SqlCommand(sqlUpdateP, conn))
        {
            cmd.Parameters.AddWithValue("@HireDate", instructor.HireDate.HasValue ? instructor.HireDate.Value : (object)DBNull.Value);
            cmd.Parameters.AddWithValue("@PersonID", instructor.PersonID);

            cmd.ExecuteNonQuery();
        }

        //
        // Inserta el registro que define al instructor concretamente
        //
        string spInsertOA = @"INSERT OfficeAssignment (InstructorID, Location) 
                                   VALUES (@InstructorID, @Location)";

        using (SqlCommand cmd = new SqlCommand(spInsertOA, conn))
        {
            cmd.Parameters.AddWithValue("@InstructorID", instructor.PersonID);
            cmd.Parameters.AddWithValue("@Location", instructor.Location);

            cmd.ExecuteNonQuery();
        }

    }

    //
    // Se procesa los cursos asignados 
    //
    CourseRepository.RelateWithPerson((PersonEntity)instructor, instructor.Courses);

}

3- En caso de existir entidades relacionadas se realiza la operación de merge entre los datos provenientes de la selección del usuario y los datos existentes en la tabla

Para realizar la tarea de forma simple se elimina toda relación y se procede a crearlas nuevamente, pero si se anima se podría haber utilizado la instrucción MERGE de T-SQL.

En la línea:

//
// Se procesa los cursos asignados 
//
CourseRepository.RelateWithPerson((PersonEntity)instructor, instructor.Courses);

Se invoca la clase responsable de crear la relación entre las entidad persona y cursos.

 

/// <summary>
/// Dada una persona y una lista de cursos se crea la relacion entre las entidades.
/// 
/// Para implementar un merge simple que permita registrar los cursos agregados o eliminados por el usuario, 
/// se realiza se elimina toda la relacion y volverla a insertar 
/// </summary>
/// <param name="person"></param>
/// <param name="courses"></param>
public static void RelateWithPerson(PersonEntity person, List<CourseEntity> courses)
{

    using (SqlConnection conn = new SqlConnection(ConfigurationManager.ConnectionStrings["default"].ToString()))
    {
        conn.Open();

        //se elimina la relacion existentes
        string sqlDelete = @"DELETE CourseInstructor WHERE PersonID = @PersonID";

        using (SqlCommand cmd = new SqlCommand(sqlDelete, conn))
        {
            cmd.Parameters.AddWithValue("@PersonID", person.PersonID);

            cmd.ExecuteNonQuery();
        }


        //se relaciona los cursos asociados a la entidad 
        string sqlCourseInstructor = @"INSERT CourseInstructor (CourseID, PersonID) 
                                            VALUES (@CourseID, @PersonID)";

        using (SqlCommand cmd = new SqlCommand(sqlCourseInstructor, conn))
        {

            foreach (CourseEntity course in courses)
            {
                cmd.Parameters.Clear();
                cmd.Parameters.AddWithValue("@CourseID", course.CourseID);
                cmd.Parameters.AddWithValue("@PersonID", person.PersonID);

                cmd.ExecuteNonQuery();
            }
        }

    }


}

Herencia de entidades (Actualizar entidad)


La actualización de une entidad que implementar una herencia es muy similar a la creación.

  1. actualizar el registro en la tabla base
  2. actualizar el registro en la tabla OfficeAssignment
  3. si la entidad tenia cursos asignados se crea la relación con esto

 

1- Se invoca al metodo Update() de PersonRepository

public static void Update(PersonEntity person)
{
    string sql = @"UPDATE Person SET
                        LastName = @LastName,
                        FirstName = @FirstName
                    WHERE PersonID = @PersonID";

    using (SqlConnection conn = new SqlConnection(ConfigurationManager.ConnectionStrings["default"].ToString()))
    {
        conn.Open();

        SqlCommand cmd = new SqlCommand(sql, conn);
        cmd.Parameters.AddWithValue("@LastName", person.LastName);
        cmd.Parameters.AddWithValue("@FirstName", person.FirstName);
        cmd.Parameters.AddWithValue("@PersonID", person.PersonID);

        cmd.ExecuteNonQuery();

    }
}

 

2 – Se actualiza la tabla que define al Instructor, este se define en el método Update() de la clase InstructorRepository.

public static void Update(InstructorEntity instructor)
{
    //
    //Se actualiza el registro de la Persona
    //
    PersonRepository.Update((PersonEntity)instructor);


    using (SqlConnection conn = new SqlConnection(ConfigurationManager.ConnectionStrings["default"].ToString()))
    {
        conn.Open();

        //
        // Actualiza los campos de la tabla Persona 
        // con el campo que define solo el instructor
        //
        string sqlUpdateP = @"UPDATE Person 
                                SET HireDate = @HireDate 
                            WHERE PersonID = @PersonID";

        using (SqlCommand cmd = new SqlCommand(sqlUpdateP, conn))
        {
            cmd.Parameters.AddWithValue("@HireDate", instructor.HireDate.HasValue ? instructor.HireDate.Value : (object)DBNull.Value);
            cmd.Parameters.AddWithValue("@PersonID", instructor.PersonID);

            cmd.ExecuteNonQuery();
        }

        //
        // Actualiza el registro que define al instructor concretamente
        //
        string sqlUpdateOA = @"UPDATE OfficeAssignment 
                                        SET Location = @Location 
                                   WHERE InstructorID = @InstructorID";

        using (SqlCommand cmd = new SqlCommand(sqlUpdateOA, conn))
        {
            cmd.Parameters.AddWithValue("@InstructorID", instructor.PersonID);
            cmd.Parameters.AddWithValue("@Location", instructor.Location);

            cmd.ExecuteNonQuery();
        }

    }

    //
    // Se procesa los cursos asignados 
    //
    CourseRepository.RelateWithPerson((PersonEntity)instructor, instructor.Courses);

}

3 – Al igual que al crear la entidad se actualizan las relaciones con las demás entidades, en este caso se aplica el mismo código para reflejar la relación con los cursos.

 

Código


 

[c#]
 

21 comentarios:

  1. Hola leandro! tengo una duda..en los foros ayer me ayudaste a remodelar mi capa de datos con entidades, y en el ejemplo te habia mostrado esta entidad:

    public class LocalidadBaseEntity
    {
    public int id_localidad { get; set; }
    public string nombre_localidad { get; set; }
    public int cod_postal { get; set; }
    public int id_provincia { get; set; }
    public string usuario { get; set; }
    }

    Si por ejemplo, en una consulta necesitaria traer el campo "nombre_provincia" de otra tabla, deberia crear otra entidad para ello no, de esta manera?

    public class LocalidadCompletaEntity
    {
    public int id_localidad { get; set; }
    public string nombre_localidad { get; set; }
    public int cod_postal { get; set; }
    public int id_provincia { get; set; }
    public string nombre_provincia { get; set; }
    }

    y por ejemplo, para las altas, modificaciones, pasaria la primera; y en el caso de querer una query mas completa, pasaria la segunda entidad...saludos!

    ResponderEliminar
  2. hola matias

    pero en este caso que planteas podrias aplicar herencia

    public class LocalidadCompletaEntity : LocalidadBaseEntity
    {
    public string nombre_provincia { get; set; }
    }

    porque la entidad completa es igual a la Base solo que agregas una propiedad adicional

    saludos

    ResponderEliminar
  3. gracias leandro! recien estoy haciendo mis primeras pruebas con entidades, y no se me habia ocurrido!

    ResponderEliminar
  4. Buen dia leandro, quiero saber si usando un gridview puedo editar/actualizar las celdas sin el uso de los botones editar/actualizar?

    ResponderEliminar
  5. hola Jose

    hasta donde se no se puede, debes contar con las opciones ya sean link o botones que permitan pasar a edicion una row, ademas de aceptar o cancelar el cambio

    saludos

    ResponderEliminar
  6. Perdon Leandro yo se que talvez la pregunta que deseo realizarte no es con respecto al tema fijate que tengo un lector QUE ES UN LECTOR DE HUELLAS MC7XFPR-01R motorola y necesito programarlo ya tengo el sdk pero todavia no encuentro como hacer que compare la huellas digitales y guardarlas de antemano muchas gracias espero tu ayuda

    ResponderEliminar
  7. hola Der8578

    pero si tienes un sdk que corresponde al modelo del lector este tiene que venir con ejemplos de codigo mostrando como usar el api para realizar comparaciones

    tambien deberia venir con algun archivo (quizas un chm) de ayuda aunque sea con la info de los metodo de las librerias que debes usar

    saludos

    ResponderEliminar
  8. hola elvis

    respondi en el mail

    saludos

    ResponderEliminar
  9. Saludos Leandro,
    Nuevamente te escribo para saber si puedes darme una idea de como puedo grabar y exportar los datos contenidos en una Listview de 7 columnas en un archivo XML. He intentado usar la Serializacion pero tengo errores por todos lados.
    En esta parte de mi aplicacion, debo hacer click sobre un boton que permita guardar el contenido de mi Listview en un archivo XML.
    Agradezco de antemano tu respuesta.

    ResponderEliminar
  10. hola Federico

    pero estas volcando los datos del listview a una clase para poder serializarla?

    o sea el mismo control no se serializa, debes levarlo a una clase

    Cómo serializar un objeto

    saludo

    ResponderEliminar
  11. Hola Leandro, tengo una duda como la que te plantea matias p.
    Supongamoa que tengo una entidad Producto que contiene entre varios atributos las siguientes FK (ID_categoria), (ID_Provedor) estan definidos como int, pero en ciertos metodos van a ser cargados con string a travez de una consulta con INNER JOIN.
    ¿Debo crear otra entidad para manejar esos casos o debo agregar una propiedad (string) adicional, a mi entidad ya existente? O En esos casos cuando tengo una entidad con una FK debo crear un List?
    Luego tengo otro duda al datasource de un datagridview le cargo la entidad producto a modo de ejemplo de sus 20 campos solo quiero mostrar 3.
    El filtrado para q me muestre solo esos 3 campos lo tengo q hacer en el dgv no? no lo estoy podiendo resolver y me preguntaba si tal vez lo este haciendo mal, tal vez no tenga que traer la entidad completa.
    Gracias por todo!

    ResponderEliminar
  12. hola Luis

    Pero porque tendrias que cargar como string un id de la entidad, eso que los metodos carguen un string no es correcto
    en el inner join usas los id como int para relacionar las tablas

    Para mostrar solo 3 campos en el grid deberias definir estas columnas en tiempo de diseño, a la columna le asignas el DatapropertyName indicando el nombre de la propiedad que mapea con esa columna
    de esta forma por mas que tengas 20 propiedades en el objeto que aisgnas al datasource el grid solo le interesaran los 3 que definas
    en la coleccion columns del grid es donde defines esto

    saludos

    ResponderEliminar
  13. Muchas Gracias por tu respuesta Leandro, el problema que tengo es que no estoy entendiendo bien la logica de las entidades.
    Sigo tus ejemplos pero con algunas cosas me pierdo.

    Es que si tengo estos campos en mi entidad producto (ProductoEntity):

    int Id_Produco (PK)
    String NombreProducto
    Int Id_Categoria (FK)

    luego creo una instancia:
    ProductoEntity producto = new ProductoEntity();

    Luego ejecuto una consulta para llenar esa entidad, pero quiero obtener el nombre de la categoria

    SELECT p.id_producto, pNombre, cNombre
    FROM Producto AS p
    INNER JOIN Categoria AS c
    ON p.id_producto = c.Idproducto

    Yo no tengo un campo string en el producto para mostrar el nombre de la categoria, eso es lo que no me cierra bien? Como se maneja eso? debo usar dos entidades?
    Te agradesco por tu respuesta y se que estoy preguntando algo tonto, pero no lo estoy entendiendo bién

    ResponderEliminar
  14. hola Luis

    aqui hay que diferenciar dos cosas, una es la entidad de negocio y otra es como muestras la informacion

    en tu entidad de negocio puede definir propiedades de navegacion que una una entidad con otra
    pero cuando llevas esto a al UI esta claro que debes aplanar los datos

    para ayudarte en esta tarea podrias crear una clase deferente en el proyecto de UI que sea

    public class ProductoModel
    {
    public int Id_Produco {get; set;}
    public String NombreProducto {get; set;}
    public int Id_Categoria {get; set;}
    public string CategoriaDesc {get; set;}
    }

    esta clase la usaria solo en UI ayudandote con automapper
    con este podrias autoamtizar la conversion de una clase a otra facilmente

    saludos

    ResponderEliminar
  15. hola...podrías decirme por que sale este error
    rimera excepción del tipo 'System.ArgumentOutOfRangeException' en mscorlib.dll

    Información adicional: El índice estaba fuera del intervalo. Debe ser un valor no negativo e inferior al tamaño de la colección.

    Si hay un controlador para esta excepción, el programa puede continuar de forma segura.

    ResponderEliminar
    Respuestas
    1. hola
      Entiendo que deebs de estar usando un array o lista y el indice que quieres acceder pasa las dimensiones que define
      Sin analizar el codigo no podria decir mucho mas, pero podrias poner un breakpoint en el codigo para inspeccionar el valor que toma las variables
      saludos

      Eliminar
  16. Leandro, buen día.
    Yo tengo una duda respecto a una relación de entidades para un datagridview.
    Quisiera que me envíes un mensaje a pepejose_14@hotmail.com para poder charlar de la mejor manera y poder entender.
    Muchos de tus tutoriales me ayudan y sigo avanzando, pero está vez necesito personalmente la ayuda.
    Espero tu pronta respuesta.

    Saludos.
    Atte. José

    ResponderEliminar
  17. Hola Leandro:

    Te agradezco por este interesante y útil tema que has tratado con respecto a la herencia de entidades.

    Me queda la duda de si la columna HireDate en la tabla Person debería estar en la tabla OfficeAssignment que trata con datos de Instructor, así como la columna EnrollmentDate que se refiere a datos de Student también debería ir en otra tabla denominada Student.

    Mucho agradecería tu explicación de tener HireDate y EnrollmentDate en Person.

    Gracias de antemano Leandro.

    Xabier

    PS. Los links para poder descargar los ejemplos no parecen funcionar correctamente. ¿Habrá algún problema?

    ResponderEliminar
    Respuestas
    1. hola
      En realidad la ubicacion de las columnas va a depender de como modeles la herencia de las entidades, hay varias formas de lograrlo, podrias definir todo en una tabla y usar una columnas discriminador para conocer si registras un instructor o un estudiante, en este caso no me enfoque en la herencia concretamente sino mas bien en las relaciones. En este caso solo registro instructores en la tabla persona.

      Aqui

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

      explico algo de esto usando entity framework, como veras las entidades mapean a una unica tabla pero se define el campo Type como discriminador de tipo.
      Puedes hacerlo de esta forma o en tablas separadas

      [Entity Framework][Code First] Herencia - Tabla por tipo - Table per Type (TPT)

      Valide los links y estan correctos, estan alojados en OneDrive, no deberia causar problemas el acceso pero por las dudas usa un browser actualizado para acceder

      saludos

      Eliminar
    2. ¡Qué tal Leandro!

      Muchas gracias por tu respuesta, respecto a las descargas he intentado con varios navegadores como Mozilla, Chrome, Opera e incluso Edge (ya que cuento con un equipo que tiene Win 10 Pro), con las versiones más recientes; pero ha sido en vano.

      En lugar del icono de la descarga aparece el texto: "No se puede cargar este elemento ahora." y si doy click me redirecciona a: "https://onedrive.live.com/about/es-es/" sin darme opción de descargar nada.

      Recuerdo que ya hace algún buen tiempo sí podía descargar los archivos, e intenté con otras versiones de Windows como XP y Win-7 y distintos navegadores; pero también todo fue en vano.

      De todas maneras lo importante es el contenido de tus artículos y con eso me quedo.

      Saludos

      Xabier

      Eliminar
    3. ¡Hola Leandro!

      Hoy acabo de intentar descargar el código y ya lo he logrado con el navegador que siempre uso que es Mozilla.

      Por otro lado, si intento descargar el código del artículo "[jQuery] RadioButton y CheckBox" no aparece el icono amarillo de descarga, y si de todas maneras hago click me redirecciona a otro página donde me aparece el error:

      "Firefox can’t find the server at www.ltuttini.com.ar."

      es decir, no me redirecciona a onedrive para la descarga.

      Tal vez haya un problema en el servidor donde se hospedan los archivos.

      Un saludo y muchas gracias por compartir tus conocimientos.

      Xabier

      Eliminar