Abstracción de datos (II): Construyendo un DAO simple en C#


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”.

090501CrearProyecto

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.

090502Estructura

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.

090503AppConfig

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:

090503bTabla

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

090504Referencia

La primera de ellas será a la biblioteca System.Configuration, para tener acceso a la cadena de conexión.

090505SystemConfiguration

En segundo lugar, agregaremos una referencia al proyecto ‘Simple’ que hemos creado previamente.

090506RefProyecto

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:

090507Contenido

El resultado final puede verse a continuación:

090508Resultado

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 &lt; dt.Rows.Count; fila++)
             {
               for (int col = 0; col &lt; 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 + &quot;:      &quot; + campo.ToString());
                 }
               }

                // Mostramos un separador
                Console.WriteLine(&quot;&quot;);
                Console.WriteLine(&quot; ------------------------------------ &quot;);
                Console.WriteLine(&quot;&quot;);
              }
            }
            catch (Exception ex)
            {
              throw (ex);
            }
         }
     }
}

7 comentarios

  1. 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.

  2. 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

Deja una respuesta

Introduce tus datos o haz clic en un icono para iniciar sesión:

Logo de WordPress.com

Estás comentando usando tu cuenta de WordPress.com. Salir /  Cambiar )

Imagen de Twitter

Estás comentando usando tu cuenta de Twitter. Salir /  Cambiar )

Foto de Facebook

Estás comentando usando tu cuenta de Facebook. Salir /  Cambiar )

Conectando a %s