Patrón UoW Parte 2

Introducción

En vista de que parece que el artículo que publique ayer sobre UoW, parecía que era demasiado simple…, y a pesar de que Lucas Ontivero ha publicado un ejemplo (Gracias), he decidido publicar un ejemplo un poco más amplio para ver su utilización.

Usando el Patrón

Partiendo de una clase base:

   1: public abstract class cBase : INotifyPropertyChanged

   2:     {

   3:         public event PropertyChangedEventHandler PropertyChanged;

   4:         public void CambiarPropiedad(string Propiedad)

   5:         {

   6:             if (PropertyChanged != null)

   7:             {

   8:                 PropertyChanged(this, new PropertyChangedEventArgs(Propiedad));

   9:             }

  10:         }

  11:         public abstract SqlCommand Insertar();

  12:         public abstract SqlCommand Modificar();

  13:         public abstract SqlCommand Eliminar();

  14:     }

La clase base va a servir como definición de nuestras entidades, va a incluir el evento que va a permitir tener un control sobre los cambios, y va a declarar como abstractos las funciones de Insertar, Modificar y Eliminar que utilizaremos un poco más adelante.

Para el ejemplo he creado una pequeña Base de Datos en SQL Server que presenta una tabla muy sencilla:

 

Para “mapear” los datos de está tabla voy a hacer uso de la clase Persona:

 

   1: public class Persona : cBase

   2:     {

   3:         private int _ID = 0;

   4:         public int ID

   5:         {

   6:             get 

   7:             { 

   8:                 return _ID; 

   9:             }

  10:             set 

  11:             {

  12:                 if (!_ID.Equals(value))

  13:                 {

  14:                     _ID = value;

  15:                     CambiarPropiedad("ID");

  16:                 }

  17:             }

  18:         }

  19:         private string _Nombre = string.Empty;

  20:         public string Nombre 

  21:         {

  22:             get

  23:             {

  24:                 return _Nombre; 

  25:             }

  26:             set

  27:             {

  28:                 if (!_Nombre.Equals(value))

  29:                 {

  30:                     _Nombre = value;

  31:                     CambiarPropiedad("Nombre");

  32:                 }

  33:             }

  34:         }

  35:         private DateTime _Fecha = DateTime.Now;

  36:         public DateTime Fecha

  37:         {

  38:             get

  39:             {

  40:                 return _Fecha;

  41:             }

  42:             set

  43:             {

  44:                 if (!_Fecha.Equals(value))

  45:                 {

  46:                     _Fecha = value;

  47:                     CambiarPropiedad("Fecha");

  48:                 }

  49:             }

  50:         }

  51:         public override SqlCommand Insertar()

  52:         {

  53:             SqlCommand command = new SqlCommand("INSERT INTO PERSONA (NOMBRE,FECHA) VALUES(@NOMBRE,@FECHA)");

  54:             command.Parameters.AddWithValue("Nombre", Nombre);

  55:             command.Parameters.AddWithValue("Fecha", Fecha);

  56:             return command;

  57:         }

  58:         public override SqlCommand Modificar()

  59:         {

  60:             SqlCommand command = new SqlCommand("UPDATE PERSONA SET NOMBRE=@NOMBRE, FECHA=@FECHA WHERE ID=@ID");

  61:             command.Parameters.AddWithValue("ID", ID);

  62:             command.Parameters.AddWithValue("Nombre", Nombre);

  63:             command.Parameters.AddWithValue("Fecha", Fecha);

  64:             return command;

  65:         }

  66:         public override SqlCommand Eliminar()

  67:         {

  68:             SqlCommand command = new SqlCommand("DELETE FROM PERSONA WHERE ID=@ID");

  69:  

  70:             command.Parameters.AddWithValue("ID", ID);

  71:             return command;

  72:         }

  73:         public static List<Persona> Obtener()

  74:         {

  75:             List<Persona> listado = new List<Persona>();

  76:             DataTable datos = new DataTable();

  77:             using (SqlConnection con = new SqlConnection(@"Data Source=.SQLEXPRESS;Initial Catalog=Concept;Integrated Security=True;Pooling=False"))

  78:             {

  79:                 using (SqlDataAdapter da = new SqlDataAdapter("SELECT ID,NOMBRE,FECHA FROM PERSONA", con))

  80:                 {

  81:                     da.Fill(datos);

  82:                 }

  83:             }

  84:             foreach (DataRow row in datos.Rows)

  85:             {

  86:                 listado.Add(new Persona () { ID=Convert.ToInt32(row["ID"]),

  87:                                              Nombre = Convert.ToString(row["Nombre"]),

  88:                                              Fecha = Convert.ToDateTime(row["Fecha"])

  89:                 });

  90:             }

  91:             return listado;

  92:         }

  93:     }

En estás clase hago la implementación de los métodos abstractos que había definido en la clase Base y que van a construir objetos de tipo SqlCommand para que posteriormente sean tratados.

 

A continuación voy a exponer una “aproximación” más elaborada que la que expuse ayer, pero que tampoco es la que realmente utilizo cuando hago uso de este patrón.

 

   1: public class UnidadDeTrabajo

   2:     {

   3:        

   4:         List<cBase> objetosNuevos;

   5:         List<cBase> objetosModificados;

   6:         List<cBase> objetosEliminados;

   7:  

   8:         public UnidadDeTrabajo()

   9:         {

  10:             objetosNuevos = new List<cBase>();

  11:             objetosModificados = new List<cBase>();

  12:             objetosEliminados = new List<cBase>();

  13:        }

  14:  

  15:         public void Añadir(cBase objNuevo)

  16:         {

  17:             if (!objetosNuevos.Contains(objNuevo))

  18:             {

  19:                 objetosNuevos.Add(objNuevo);

  20:             }

  21:         }

  22:  

  23:         public void Eliminar(cBase objEliminado)

  24:         {

  25:             if (!objetosEliminados.Contains(objEliminado))

  26:             {

  27:                 objetosEliminados.Add(objEliminado);

  28:             }

  29:         }

  30:  

  31:         public void Modificar(cBase objModificado)

  32:         {

  33:             if (!objetosModificados.Contains(objModificado))

  34:             {

  35:                 objetosModificados.Add(objModificado);

  36:             }

  37:         }

  38:         public void Confirmar()

  39:         {

  40:             SqlCommand cmd;

  41:             using (TransactionScope scope = new TransactionScope())

  42:             {

  43:                 using (SqlConnection con = new SqlConnection(@"Data Source=.SQLEXPRESS;Initial Catalog=Concept;Integrated Security=True;Pooling=False"))

  44:                 {

  45:                     con.Open();

  46:                     foreach (cBase obj in objetosNuevos)

  47:                     {

  48:                         cmd = obj.Insertar();

  49:                         cmd.Connection = con;

  50:                         cmd.ExecuteNonQuery();

  51:                     }

  52:                     foreach (cBase obj in objetosModificados)

  53:                     {

  54:                         cmd = obj.Modificar();

  55:                         cmd.Connection = con;

  56:                         cmd.ExecuteNonQuery();

  57:                     }

  58:                     foreach (cBase obj in objetosEliminados)

  59:                     {

  60:                         cmd = obj.Eliminar();

  61:                         cmd.Connection = con;

  62:                         cmd.ExecuteNonQuery();

  63:                     }

  64:                     

  65:                 }

  66:                 scope.Complete();

  67:                 Limpiar();

  68:             }

  69:         }

  70:  

  71:         private void Limpiar()

  72:         {

  73:             objetosNuevos.Clear();

  74:             objetosModificados.Clear();

  75:             objetosEliminados.Clear();

  76:         }

  77:     }

La función que realmente interesa de la clase UnidadDeTrabajo es Confirmar. Como podéis observar “solo” se declara una conexión, y “solo” se abre una vez, para ejecutar todos los comandos existentes en las listas de objetos nuevos, objetos modificados y objetos eliminados.

Para ver un pequeño ejemplo de su uso:

   1: var personas = Persona.Obtener();

   2:            

   3:            UnidadDeTrabajo unit = new UnidadDeTrabajo();

   4:            unit.Eliminar(personas.Where(n=>n.Nombre.ToUpper().Equals("PRUEBA")).FirstOrDefault());

   5:            personas.Where(n => n.Nombre == "Javier").FirstOrDefault().Nombre = "JTP";

   6:            unit.Modificar(personas.Where(n => n.Nombre.ToUpper().Equals("JTP")).FirstOrDefault());

   7:            unit.Añadir(new Persona() { Nombre = "Test", Fecha = DateTime.Now });

   8:            unit.Confirmar();

 

En respuesta a los comentarios del anterior post

Creo que este ejemplo deja claro, el tener un conjunto de datos que se van a tratar de una “tacada” a la hora de ser enviados a la base de datos.

Con respecto a las relaciones, independientemente que usemos EF o no, tendremos que tener los métodos adecuados para tratar dichas relaciones si es que se realizan desde la aplicación, quizás existen triggers para manejar dichos aspectos (aunque no sea una cosa que me gusten en exceso, puede resultar fácil).

 

Bueno espero que este Post os guste más que el anterior.

 

Saludos

 

16 comentarios sobre “Patrón UoW Parte 2”

  1. Entiendo que el ejemplo, muestra el uso del patrón, sin embargo me gustaría comentar un par de factores que no me gustan de este ejemplo. Pienso que no es adecuado introducir en el código de la entidad las sentencias SqlCommand, esto crea dependencias con clases de acceso a datos de Sql, entiendo que estas normalmente deberían alojarse en un proyecto o clase diferente para eliminar estas dependencias, creo que la entidad no debe conocer cómo se va a actualizar, borrar o insertar en la base de datos, esto corresponde a las clases de datos y deberían tratarse de forma independiente, por otra parte escribir los nombres de los campos dentro de una cadena continua siendo un error muy habitual ya que si alteramos el nombre en la tabla de la base de datos no detectaremos el error, almacenar la conexión en una cadena también es un problema de seguridad que debemos evitar.

    Un saludo.

  2. @Ivan y @Manuel Gracias.
    @Juan, cuando escribi el artículo sabia que lo de la cadena de conexion sabia que iba a traer cola, pero esto es un «mero» ejemplo no he querido entrar a un nivel maximo.
    Respecto de los campos de los nombres, personalmente prefiero utilizar los nombres de los campos siempre, si hay un cambio en la BD, para mi siempre será necesario comprobar que la aplicación funciona…
    Y con respecto a los SQLCommand, no queria complicar el ejemplo, haciendo referencia a otras DLL, y existe otras formas de hacerlo.
    Saludos, y gracias por comentar!

  3. Hola Javier,

    Primero, buen artículo ya que se ha entendido bastante bien de lo que consta el UnitOfWork. Una sugerencia/pregunta… se podría mejorar mediante genéricos la clase Unidad de trabajo?

  4. @Fernando, si lo podrías aplicar:
    public class UnidadDeTrabajo where T : cBase y la definición sería:
    UnidadDeTrabajo unidad=new UnidadDeTrabajo()

    Pero… tienes que evaluar el sentido que tendría. Yo en principio no se lo veo mucho a no ser que los datos tengan que ir a distintos destinos que vengan dados por alguna peculiriadad de T…

  5. @Javier, si cuando realizas un cambio en la base de datos tienes que probar de nuevo toda la aplicación perderás mucho tiempo, sobre todo si la base de datos es grande y sufre muchos cambios, esto se puede solucionar fácilmente utilizando un enumerador con los nombres de los campos en la entidad o utilizando técnicas de reflexión para generar las sentencias sql.

    Por otra parte utilizar generics y reflexión puede tener mucho sentido si quieres generalizar algunas sentencias insert, update y delete evitando tener que escribir cada sentencia sql en base a la entidad que utilizas, no te digo nada si tienes que escribir esto con entidades que tengan muchos campos, y si encima quieres verificar si el contenido de los campos a sido alterado por otro usuario, no solo tenemos el coste de escribir el código, también debemos mantenerlo.

    Un saludo.

  6. @Juan Si haces un cambio en la aplicación, no ejecutas todo el conjunto de pruebas sobre la misma para comprobar que todo está como debe?

    Con respecto a lo de generics y reflection, quiero recalcar que esto es un «ejemplo» y sencillo, evidentemente podría haber hecho el ejemplo usando Reflection para obtener los tipos de la BD, incluso, podría haber definido una T4 para auto generarme las clases…

  7. Javier, no es mi intención criticar o discutir sobre el ejemplo que has escrito, entiendo que lo has simplificado para mostrar el patrón, tan solo, quiero exponer algunas dudas al escribir código sobre todo para aquellos que apliquen este ejemplo sin más, algo muy habitual.

    Por otra parte, he comentado el uso de generics y reflexión en base a la pregunta de Fernando Urkijo y no para criticar el ejemplo.

    Cuando haces un cambio en la aplicación, lo lógico es que ejecutes las pruebas unitarias, siempre que las tengas, la mayoría de los desarrollos que veo, no utilizan pruebas de base de datos, ni siquiera en las entidades, con lo que se hace difícil detectar errores por cambios en la estructura de las tablas. Con EF esto no se hace tan imprescindible pues los cambios en las entidades se reflejan cuando actualizas el modelo y estos errores se detectan en tiempo de compilación.

    Un saludo.

  8. Hoña Javier, excelente articulo.

    Una duda, cuando usas EF, Linq2sql o cualquier otro ORM también te creas una clase para la unidad de trabajo? Lo pregunto porque el contexto casi siempre es una implementación del patrón UoW.

    Yo por mi parte, siempre heredo del contexto y la nueva clase seria la Unidad de trabajo, pero me gustaría saber como lo desarrolla el resto…

    Gracias

  9. @Omar, lo primero gracias por contestar.
    Exacto, en principio el contexto es la Unidad de Trabajo en el caso de los ORM, por lo que bajo mi punto de vista no sería necesario crear una nueva clase, a no ser que quieras realizar comprobaciones más exhautivas de los datos que se tienen que enviar o no( y aun así no sería 100% necesaria una nueva clase que fuera la UoW)

    Saludos!

  10. Si Javier, yo lo hago por un tema de IoC y por mantener una uniformidad en mis repositorios respecto a la unidad de trabajo.

    Yo tengo definida una interfaz para mi UoW, y todos mis proyectos trabajan de la misma manera con la unidad de trabajo sin tener en cuenta si es EF, Linq2sql, NH u otra cosa pq trabajan con la interfaz q tengo….

    La clase q me creo hereda del contexto e implementa la interfaz…

    De todas maneras estoy de acuerdo en q la mayoría de los casos el propio contexto vale…

    Gracias por la aclaración 😉

Responder a anonymous Cancelar respuesta

Tu dirección de correo electrónico no será publicada. Los campos obligatorios están marcados con *