Como introducción a la aplicación práctica de este patrón que ya vimos previamente, daremos las indicaciones necesarias para construir un DAO simple para una entidad en concreto.
El ejemplo que mostraremos ahora se tratará de un elemento macizo y poco funcional, que posteriormente iremos retocando y refinando para alcanzar un nivel de abstracción lo suficientemente elevado como para exportarlo a un componente plenamente reutilizable.
Configuración del entorno.
Comenzaremos creando una nueva solución en blanco en Visual Studio, donde crearemos dos proyectos: una biblioteca de clases a la que llamaremos “Simple” y una aplicación de consola orientada a probar la biblioteca, a la que llamaremos “Pruebas”.
A continuación crearemos dos carpetas en nuestro proyecto de biblioteca de clases: Una carpeta llamada DAO y otra llamada DTO. En las carpetas crearemos dos clases. En DAO añadiremos la clase DAOUsuario y en DTO, la clase DTOUsuario.
Como siguiente paso, crearemos un fichero de configuración, el App.config (en un proyecto web sería web.config). Este fichero irá incluido en el proyecto de consola destinado a probar la aplicación, no en la biblioteca de clases.
El fichero de configuración posee información relativa al funcionamiento de la aplicación que, como su nombre indica, puede ser configurado sin necesidad de modificar código fuente. De momento, añadiremos una cadena de conexión que apunte a una base de datos local en la que tendremos una base de datos llamada “TestDB”, en la que existirá una tabla “Usuario” con la siguiente estructura:
El código DDL necesario para crear la tabla sería el siguiente:
CREATE TABLE [dbo].[Usuario]( [IdUsuario] [int] IDENTITY(1,1) NOT NULL, [Login] [varchar](50) NOT NULL, [Password] [varchar](256) NOT NULL, [Nombre] [varchar](50) NOT NULL, [Apellido1] [varchar](50) NOT NULL, [Apellido2] [varchar](50) NULL )
El fichero app.config, pues, tendría el siguiente contenido:
Hecho esto, podemos comenzar a codificar nuestro esbozo de arquitectura de acceso a datos.
Clases de intercambio de datos
Una clase que cambiará muy poco a lo largo de la evolución de nuestro patrón DAO va a ser el objeto DTO. Un DTO actuará como contenedor temporal de información, tanto para enviar datos a la fuente de datos, como para recibirlos. Su estructura será simple: una propiedad pública con el mismo nombre que cada uno de los campos de la tabla a la que “calca”, de un tipo equivalente, y que encapsulará un atributo privado, también del mismo tipo.
El motivo de añadir propiedades como recubrimiento de los atributos responde a la intención de respetar el principio de encapsulación, evitando el acceso directo a los atributos de un objeto. En cuanto a nombrar las propiedades de la misma forma que las columnas, responde a un doble objetivo: claridad del código y (como veremos posteriormente) facilitar la abstracción, al no necesitar acceder a la fuente de datos para conocer el nombre del registro al que se quiere acceder (ya que lo tendremos en el propio DTO). Así, el DTOUsuario estaría codificado de la siguiente forma:
using System; using System.Collections.Generic; using System.Text; namespace Simple.DTO { /// <summary> /// Clase que representa a la entidad Usuario, almacenada en una fuente de datos /// </summary> /// /// Daniel García 13/05/2009 Creación /// Public class DTOUsuario { #region Atributos private int? idusuario = null; private string login = null; private string password = null; private string nombre = null; private string apellido1 = null; private string apellido2 = null; #endregion #region Propiedades public int? IdUsuario { get { return idusuario; } set { idusuario = value; } } public string Login { get { return login; } set { login = value; } } public string Password { get { return password; } set { password = value; } } public string Nombre { get { return nombre; } set { nombre = value; } } public string Apellido1 { get { return apellido1; } set { apellido1 = value; } } public string Apellido2 { get { return apellido2; } set { apellido2 = value; } } #endregion #region Constructores public DTOUsuario() { } #endregion } }
Vemos que cada uno de los atributos tiene una propiedad asociada:
private int? idusuario = null; public int? IdUsuario { get { return idusuario; } set { idusuario = value; } }
Otro detalle a tener en cuenta, y que puede causar confusión en el caso de que no se conozca es el signo de interrogación ‘?’ después de declarar el atributo y la propiedad IdUsuario. Esto se debe a que estamos declarando, en lugar de un entero normal y corriente, una estructura Nullable que representa todos los valores de un entero más el valor NULL, de modo que podemos tomar este valor al obtener e insertar datos en la fuente de datos. Las variables de tipo string no requieren éste símbolo, ya que son de por sí referencias, y por lo tanto, pueden igualarse a null.
Clase DAO.
Como siguiente paso, codificaremos el contenido de nuestro DAO. Insisto de forma especial en que este modelo no debería tomarse de referencia a la hora de desarrollar un producto, ya que su interés es simplemente didáctico. Su estructura monolítica establece un excesivo acoplamiento entre el modelo de datos y la lógica de negocio, por lo que posteriormente iremos refinando este modelo en beneficio de una mayor abstracción.
Lo primero que deberemos hacer será incluir dos referencias (click derecho, agregar referencia).
La primera de ellas será a la biblioteca System.Configuration, para tener acceso a la cadena de conexión.
En segundo lugar, agregaremos una referencia al proyecto ‘Simple’ que hemos creado previamente.
A continuación deberemos incluir los espacios de nombres que utilizaremos. Éstos serán los siguientes:
using System; using System.Collections.Generic; using System.Text; // Añadimos los espacios de nombres necesarios para trabajar con SQL Server using System.Data; using System.Data.SqlClient; // Usaremos un StringBuilder, así que incluiremos System.Text. // Dado que necesitaremos un ArrayList, también añadiremos System.Collections. using System.Text; using System.Collections; // Añadimos el espacio de nombres en el que almacenamos los DTOs using Simple.DTO; <pre>
Tendremos ahora el siguiente cuerpo de nuestra clase DAOUsuario:
namespace Simple.DAO { public class DAOUsuario { } }</pre> <pre>
Vamos a crear una función cuyo objetivo sea conseguir un objeto de la clase SqlCommand perfectamente configurada para realizar una consulta. Su firma será la siguiente:
private SqlCommand ObtenerOrdenSql(string sentenciaSQL, ArrayList Parametros) { }
Devolveremos un objeto SqlCommand (que ejecuta una acción sobre una base de datos ) pasándole como parámetros la sentencia SQL a ejecutar (por ejemplo, “SELECT * FROM Usuario”) y una lista con los parámetros (pares claves-valor con el nombre y el contenido de cada parámetro). Los pasos que ejecutará esta función serán los siguientes:
Obtener una conexión a partir de la cadena de conexión alojada en el fichero de configuración. Recordemos que el nombre que le dimos a la cadena fue ‘SqlServer’. Por lo tanto, la línea de código encargada de obtener una conexión a la base de datos será esta:
SqlConnection conexion = new SqlConnection(System.Configuration.ConfigurationManager.ConnectionStrings["SqlServer"].ConnectionString);
Después instanciaremos un SqlCommand que ejecutará la sentencia que le pasemos como parámetro. Además, indicaremos que queremos ejecutar texto (una cadena con la sentencia).
SqlCommand orden = new SqlCommand(sentenciaSQL, conexion); orden.CommandType = CommandType.Text;
Por último, recorreremos la lista de parámetros elemento a elemento, insertando los parámetros en el objeto SqlCommand. Hecho esto, podremos devolver el resultado.
foreach (SqlParameter p in Parametros) orden.Parameters.Add(p); return orden;
Función de Consulta
Ahora que tenemos el qué y el quién, hace falta decir el cómo. Construiremos una función que nos devuelva un DataSet que contenga una tabla con los resultados de los filtros introducidos en el objeto DTO. Su firma será la siguiente:
public DataSet select(DTOUsuario dto) { }
Para realizar la consulta, necesitaremos declarar unos cuantos objetos. Estos serán:
– Un DataSet para alojar el contenido de la consulta.
– Un StringBuilder (constructor de cadenas) para construir la sentencia SQL de forma dinámica.
– Un StringBuilder para añadir los filtros que introduciremos, también de forma dinámica.
– Un ArrayList, que añadirá un parámetro por cada filtro.
DataSet ds = new DataSet(); StringBuilder SQLString = new StringBuilder(); StringBuilder Campos = new StringBuilder(); ArrayList Parametros = new ArrayList();
El objetivo es recorrer cada una de las propiedades de nuestro objeto. En el caso de que alguna de éstas no sea nula, añadiremos el filtro a la sentencia (por ejemplo, “login = @login”). Hay que explicar que el símbolo “@” en la cadena SQL simboliza una variable. Esa variable adquirirá el valor que le indiquemos en cada uno de sus parámetros, y se indicará en un par clave valor. Si por ejemplo añadimos un SqlParameter con argumentos (“@login”, “administrador”), la base de datos se encargará de sustituir cualquier aparición de “@login” dentro de la cadena de texto por su valor “administrador”.
La razón de no indicar el valor de forma directa (por ejemplo, haciendo “login = ‘administrador’) es evitar los posibles ataques de inyección de código SQL, concepto que veremos en otra ocasión.
Por lo tanto, lo primero que haremos será definir la parte inicial de la sentencia SQL y comprobar que el objeto dto existe.
SQLString.Append("SELECT IdUsuario, Login, Nombre, Apellido1, Apellido2 FROM Usuario "); if (dto == null) throw new NullReferenceException("DAOUsuario.select(dto)");
A continuación, recorreremos los elementos del DTO, añadiendo los filtros en el caso de que estos existan. Esto será así para cada uno de los elementos del DTO.
if (dto.IdUsuario != null) { // Añadimos la sentencia SQL Campos.Append("IdUsuario = @IdUsuario AND "); // Añadimos un nuevo parámetro a la lista de parámetros Parametros.Add(new SqlParameter("@IdUsuario", (object)dto.IdUsuario)); } // Realizamos la misma operación para el resto de parámetros if (dto.Login != null) { Campos.Append("Login = @Login AND "); Parametros.Add(new SqlParameter("@Login", (object)dto.Login)); } if (dto.Nombre != null) { Campos.Append("Nombre = @Nombre AND "); Parametros.Add(new SqlParameter("@Nombre", (object)dto.Nombre)); } if (dto.Apellido1 != null) { Campos.Append("Apellido1 = @Apellido1 AND "); Parametros.Add(new SqlParameter("@Apellido1", (object)dto.Apellido1)); } if (dto.Apellido2 != null) { Campos.Append("Apellido2 = @Apellido2 AND "); Parametros.Add(new SqlParameter("@Apellido2", (object)dto.Apellido2)); }
En caso de que se haya introducido algún filtro (“Campos” tenga algún elemento) significará que deberemos introducir la cláusula “WHERE” en la sentencia SQL, y a continuación los campos correspondientes. Nos daremos cuenta, además, de que como todos los elementos insertados en el StringBuilder “Campos” finalizan con “AND “, será necesario eliminar el último de ellos para que la sentencia tenga sentido.
// Finalmente, añadimos la cláusula WHERE, en caso de que sea necesaria if (Campos.Length > 0) { SQLString.Append("WHERE "); // Como la cadena acabará en 'AND ', eliminamos los últimos 4 caracteres para cerrar la sentencia SQLString.Append(Campos.ToString().Substring(0, Campos.ToString().Length - 4)); }
Finalmente, instanciamos un DataAdapter y lo utilizamos para rellenar el DataSet, que será devuelto con la función.
// Instanciamos un SqlDataAdapter y efectuamos la consulta SqlDataAdapter da = new SqlDataAdapter(orden); da.Fill(ds); // Por último, devolvemos el DataSet return ds;
Por su parte, el proyecto de consola se limitará a instanciar un DTO, rellenar sus filtros y pasárselos al DAO para obtener el resultado.
DTOUsuario dto = new DTOUsuario(); DAOUsuario dao = new DAOUsuario(); DataSet ds; dto.IdUsuario = 1; dto.Login = "admin"; dto.Password = "admin"; ds = dao.select(dto); if (ds != null) { MostrarTabla(ds.Tables[0]); } Console.ReadLine();
El resultado de la prueba sería la obtención del registro que cumpla con que los campos indicados coincidan con los valores indicados, según vemos en la depuración:
El resultado final puede verse a continuación:
Por último, el código fuente completo de las tres clases utilizadas en este ejemplo:
DTOUsuario.cs
/* * DTOUsuario.cs * Objeto de Transferencia de Datos asociada a una Entidad * * (CC) 2009 Daniel Garcia * Some Rights Reserved. */ using System; using System.Collections.Generic; using System.Text; namespace Simple.DTO { /// <summary> /// Clase que representa a la entidad Usuario, almacenada en una fuente de datos /// </summary> /// /// Daniel García 13/05/2009 Creación /// public class DTOUsuario { #region Atributos private int? idusuario = null; private string login = null; private string password = null; private string nombre = null; private string apellido1 = null; private string apellido2 = null; #endregion #region Propiedades public int? IdUsuario { get { return idusuario; } set { idusuario = value; } } public string Login { get { return login; } set { login = value; } } public string Password { get { return password; } set { password = value; } } public string Nombre { get { return nombre; } set { nombre = value; } } public string Apellido1 { get { return apellido1; } set { apellido1 = value; } } public string Apellido2 { get { return apellido2; } set { apellido2 = value; } } #endregion #region Constructores public DTOUsuario() { } #endregion } }
DAOUsuario.cs
/* * DAOUsuario.cs * Objeto de Acceso a Datos asociada a una Entidad * * (CC) 2009 Daniel Garcia * Some Rights Reserved. */ using System; using System.Collections.Generic; using System.Text; // Añadimos los espacios de nombres necesarios para trabajar con SQL Server using System.Data; using System.Data.SqlClient; // Usaremos un StringBuilder, así que incluiremos System.Text. // Dado que necesitaremos un ArrayList, también añadiremos System.Collections. using System.Text; using System.Collections; // Añadimos el espacio de nombres en el que almacenamos los DTOs using Simple.DTO; namespace Simple.DAO { public class DAOUsuario { /// /// Función que realizará una consulta a base de datos, devolviendo los registros que /// cumplan con los criterios establecidos en el DTO pasado como parámetro. /// /// Daniel García 13/05/2009 Creación /// public DataSet select(DTOUsuario dto) { try { // Instanciamos un DataSet, que albergará el contenido de la consulta DataSet ds = new DataSet(); // Declaramos dos StringBuilders: uno para la sentencia y otra para los campos // A su vez, declaramos un ArrayList para almacenar los parámetros. StringBuilder SQLString = new StringBuilder(); StringBuilder Campos = new StringBuilder(); ArrayList Parametros = new ArrayList(); // Comenzamos con la declaración de la sentencia. SQLString.Append("SELECT IdUsuario, Login, Nombre, Apellido1, Apellido2 FROM Usuario "); // Comprobamos que el DTO exista if (dto == null) throw new NullReferenceException("DAOUsuario.select(dto)"); // A continuación, recorremos los elementos del DTO, añadiendo los filtros en el caso //de que éstos existan. if (dto.IdUsuario != null) { // Añadimos la sentencia SQL Campos.Append("IdUsuario = @IdUsuario AND "); // Añadimos un nuevo parámetro a la lista de parámetros Parametros.Add(new SqlParameter("@IdUsuario", (object)dto.IdUsuario)); } // Realizamos la misma operación para el resto de parámetros if (dto.Login != null) { Campos.Append("Login = @Login AND "); Parametros.Add(new SqlParameter("@Login", (object)dto.Login)); } if (dto.Nombre != null) { Campos.Append("Nombre = @Nombre AND "); Parametros.Add(new SqlParameter("@Nombre", (object)dto.Nombre)); } if (dto.Apellido1 != null) { Campos.Append("Apellido1 = @Apellido1 AND "); Parametros.Add(new SqlParameter("@Apellido1", (object)dto.Apellido1)); } if (dto.Apellido2 != null) { Campos.Append("Apellido2 = @Apellido2 AND "); Parametros.Add(new SqlParameter("@Apellido2", (object)dto.Apellido2)); } // Finalmente, añadimos la cláusula WHERE, en caso de que sea necesaria if (Campos.Length > 0) { SQLString.Append("WHERE "); // Como la cadena acabará en 'AND ', eliminamos los últimos 4 caracteres para cerrar la sentencia SQLString.Append(Campos.ToString().Substring(0, Campos.ToString().Length - 4)); } // Obtenemos un SqlCommand configurado al efecto SqlCommand orden = ObtenerOrdenSql(SQLString.ToString(), Parametros); // Instanciamos un SqlDataAdapter y efectuamos la consulta SqlDataAdapter da = new SqlDataAdapter(orden); da.Fill(ds); // Por último, devolvemos el DataSet return ds; } catch (Exception ex) { throw (ex); } } /// Configura un SqlCommand para ejecutar una sentencia con unos parámetros específicos /// La conexión tomada por defecto es aquella configurada como 'SqlServer' en el fichero /// de configuración. /// Daniel García 13/05/2009 Creación private SqlCommand ObtenerOrdenSql(string sentenciaSQL, ArrayList Parametros) { try { // Creamos una conexión a partir de la ConnectionString del web.config SqlConnection conexion = new SqlConnection(System.Configuration.ConfigurationManager.ConnectionStrings["SqlServer"].ConnectionString); // Instanciamos un SqlCommand que ejecutará la sentencia que le pasemos como parámetro con la conexión. SqlCommand orden = new SqlCommand(sentenciaSQL, conexion); //Configuramos el SqlCommand,indicando que ejecutará una sentencia e inyectándole los parámetros orden.CommandType = CommandType.Text; foreach (SqlParameter p in Parametros) orden.Parameters.Add(p); // Finalmente, devolvemos el SqlCommand return orden; } catch (Exception ex) { throw (ex); } } } }
Por último, el programa de prueba para la biblioteca.
Programa.cs
/* * Program.cs * Programa de prueba de Simple.DAO * * (CC) 2009 Daniel Garcia * contacto {at} danigarcia {dot} org * Some Rights Reserved. */ using System; using System.Collections.Generic; using System.Text; using System.Data; using Simple.DTO; using Simple.DAO; namespace Pruebas { class Program { static void Main(string[] args) { // Declaramos un DTO de la entidad Usuario DTOUsuario dto = new DTOUsuario(); // Declaramos un DAO para la entidad Usuario DAOUsuario dao = new DAOUsuario(); // El DataSet se encargará de obtener los datos de respuesta DataSet ds; // Rellenamos filtros dto.IdUsuario = 1; dto.Login = "admin"; dto.Password = "admin"; // Invocamos el método 'select' del DAO pasándole el DTO como parámetro ds = dao.select(dto); if (ds != null) { // Mostramos el contenido de la primera tabla devuelta MostrarTabla(ds.Tables[0]); } Console.ReadLine(); } /// <summary> /// Método encargado de visualizar un DataTable por consola /// </summary> static void MostrarTabla(DataTable dt) { try { int numColumnas; string nombreCampo = ""; string campo = ""; numColumnas = dt.Columns.Count; // Iteramos por cada fila y cada columna for (int fila = 0; fila < dt.Rows.Count; fila++) { for (int col = 0; col < numColumnas; col++) { if (!dt.Rows[fila].IsNull(col)) { // Almacenamos el nombre del campo y su contenido campo = dt.Rows[fila][col].ToString(); nombreCampo = dt.Columns[col].ColumnName; } if (campo != null) { // Mostramos por consola ambos datos Console.WriteLine(nombreCampo + ": " + campo.ToString()); } } // Mostramos un separador Console.WriteLine(""); Console.WriteLine(" ------------------------------------ "); Console.WriteLine(""); } } catch (Exception ex) { throw (ex); } } } }
Nice.
Veremos que tal sigue el próximo articulo.
Saludos!
Como puedo hacer el DAO Dimanico,al momento de hacer un catalogo(altas,bajas y cambios) en un solo metodo.
Ercelente!
Un ejemplo básico y practico.
Seria bueno encapsular en una Clase lo que es base de datos aplicando inversión de control / inyección de dependencias.
AMIGO ME DA EL SIGUIENTE ERROR
{«Error en la inicialización del sistema de configuración»}
LA UNICA DIFERENCIA QUE TENEMOS ES QUE YO NO LE TENGO CLAVE NI USUARIO A LA BD