Arquitectura – Definición de un Data Access Component (con un ejemplo) Parte 1

Hola, qué tal.

En esta ocasión es momento de entrar un poco en materia e iniciar con la aplicación de DataHelper en el desarrollo de aplicaciones. Continuando el orden de jerarquías, o bien, la secuencia inversa de la propuesta de arquitectura, una vez definido el helper, debemos tener una base de datos entre el helper y el componente de acceso a datos por lo que nos daremos a la tarea de crear una base de datos llamada “EjemploDAC”, la cual contendrá una tabla de ejemplo llamada “TipoProducto”. A continuación pongo el código para simplificar esta tarea en SQL Server:

T-SQL:

CREATE DATABASE [EjemploDAC] ON  PRIMARY

(

      NAME = N’EjemploDAC’,

      FILENAME = N’C:\MSSQL\DATA\EjemploDAC.mdf’,

      SIZE = 3072KB ,

      FILEGROWTH = 1024KB )

 LOG ON

(

      NAME = N’EjemploDAC_log’,

      FILENAME = N’C:\MSSQL\DATA\EjemploDAC_log.ldf’,

      SIZE = 1024KB ,

      FILEGROWTH = 10%)

GO

ALTER DATABASE [EjemploDAC] SET COMPATIBILITY_LEVEL = 100

GO

ALTER DATABASE [EjemploDAC] SET ANSI_NULL_DEFAULT OFF

GO

ALTER DATABASE [EjemploDAC] SET ANSI_NULLS OFF

GO

ALTER DATABASE [EjemploDAC] SET ANSI_PADDING OFF

GO

ALTER DATABASE [EjemploDAC] SET ANSI_WARNINGS OFF

GO

ALTER DATABASE [EjemploDAC] SET ARITHABORT OFF

GO

ALTER DATABASE [EjemploDAC] SET AUTO_CLOSE OFF

GO

ALTER DATABASE [EjemploDAC] SET AUTO_CREATE_STATISTICS ON

GO

ALTER DATABASE [EjemploDAC] SET AUTO_SHRINK OFF

GO

ALTER DATABASE [EjemploDAC] SET AUTO_UPDATE_STATISTICS ON

GO

ALTER DATABASE [EjemploDAC] SET CURSOR_CLOSE_ON_COMMIT OFF

GO

ALTER DATABASE [EjemploDAC] SET CURSOR_DEFAULT  GLOBAL

GO

ALTER DATABASE [EjemploDAC] SET CONCAT_NULL_YIELDS_NULL OFF

GO

ALTER DATABASE [EjemploDAC] SET NUMERIC_ROUNDABORT OFF

GO

ALTER DATABASE [EjemploDAC] SET QUOTED_IDENTIFIER OFF

GO

ALTER DATABASE [EjemploDAC] SET RECURSIVE_TRIGGERS OFF

GO

ALTER DATABASE [EjemploDAC] SET  DISABLE_BROKER

GO

ALTER DATABASE [EjemploDAC] SET AUTO_UPDATE_STATISTICS_ASYNC OFF

GO

ALTER DATABASE [EjemploDAC] SET DATE_CORRELATION_OPTIMIZATION OFF

GO

ALTER DATABASE [EjemploDAC] SET PARAMETERIZATION SIMPLE

GO

ALTER DATABASE [EjemploDAC] SET  READ_WRITE

GO

ALTER DATABASE [EjemploDAC] SET RECOVERY FULL

GO

ALTER DATABASE [EjemploDAC] SET  MULTI_USER

GO

ALTER DATABASE [EjemploDAC] SET PAGE_VERIFY CHECKSUM 

GO

USE [EjemploDAC]

GO

IF NOT EXISTS (SELECT name

                     FROM sys.filegroups

                     WHERE is_default=1 AND name = N’PRIMARY’)

      ALTER DATABASE [EjemploDAC]

      MODIFY FILEGROUP [PRIMARY] DEFAULT

GO

 

USE [EjemploDAC]

GO

 

/****** Object:  Table [dbo].[TipoProducto] ******/

SET ANSI_NULLS ON

GO

 

SET QUOTED_IDENTIFIER ON

GO

 

SET ANSI_PADDING ON

GO

 

CREATE TABLE [dbo].[TipoProducto]

(

      [IdTipoProducto] [int] IDENTITY(1,1) NOT NULL,

      [ClaveTipoProducto] [varchar](10) NOT NULL,

      [Descripcion] [varchar](150) NOT NULL,

       CONSTRAINT [PK_TipoProducto] PRIMARY KEY CLUSTERED

      (

            [IdTipoProducto] ASC

      )

      WITH

      (

            PAD_INDEX  = OFF,

            STATISTICS_NORECOMPUTE  = OFF,

            IGNORE_DUP_KEY = OFF,

            ALLOW_ROW_LOCKS  = ON,

            ALLOW_PAGE_LOCKS  = ON

      ) ON [PRIMARY]

) ON [PRIMARY]

GO

 

SET ANSI_PADDING OFF

GO

 

Una vez creada la base de datos procedamos a definir los conceptos de lo que será nuestro componente de acceso a datos.

He creado una tabla llamada TipoProducto, adjunta a la base de datos, comprende de un campo identity y de dos campos varchar.

Bien, teniendo esto, vamos a empezar por la primera regla de los componentes de acceso a datos:

1-      Todas las tablas deben contar con su contraparte abstracta en la aplicación, esto es, un componente de acceso a datos (DAC) por cada tabla.

Así entonces procederemos a abstraer la tabla en un componente de software que exponga la funcionalidad necesaria a la aplicación para las tareas comunes con los datos de esta.

La tarea de abstracción puede ser sencilla sabiendo por dónde empezar, primeramente entendemos que la tabla está definida por campos, mismos que formarán un registro, lo que buscamos con un componente de acceso a dato es tener la abstracción de la tabla y la conformación del registro en la misma clase, esto es, tener una clase que defina de manera abstracta el registro de la tabla además de poder ejercer sobre este las tareas básicas de la manipulación de datos, esto es, poder crear, leer, actualizar y borrar un registro en la tabla de la base de datos por medio del componente, tal como si del propio registro se tratase.

En realidad la el componente actúa como registro y a la vez tendrá la capacidad de exponer un mecanismo para devolver una colección de registros, ya sea como un DataTable, o como una lista genérica con el componente como parámetro de tipo.

Las reglas o características subsecuentes se describen a continuación:

2-      El componente de acceso a datos debe contener todos los campos de la tabla que representa a manera de propiedades, mapeando los tipos de datos.

3-      El componentes de acceso a datos debe implementar la funcionalidad básica para CRUD (create, read, update, delete) Crear, leer, actualizar y borrar.

4-      El componente de acceso a datos debe ser capaz de utilizarse como un registro.

5-      El componente de acceso a datos debe ser capaz de devolver una colección de registro de la tabla que representa.

a.      Se puede devolver un resultado de registro como un DataTable

b.      Se puede devolver un resultado de registro como una colección de objetos del mismo tipo que del tipo de acceso a datos. Comúnmente se utiliza una lista genérica con un parámetro de tipo, este parámetro es del tipo del componente de acceso a datos.

6-      El componente de acceso a datos contendrá todas las consultas relacionadas con la tabla que representa, esto es, toda consulta donde exista la tabla que representa el componente de acceso a datos después de un from, debe está expuesta por el componente mismo de la tabla.

Todo esto lo veremos con nuestro ejemplo, pongámosle nombre a todo y también el código, de una buena vez:

Empecemos por la definición de la clase, primeramente quiero hacer hincapié en el DataHelper, mismo que será la base para todas nuestros componentes de acceso a datos. Recordaremos que el DataHelper está definido como una clase abstracta, esto con la finalidad de exponer su funcionalidad a través de la herencia, o bien, al ser derivada en las clases de acceso a datos, mismas que explotarán la funcionalidad del DataHelper. Cabe mencionar que crearé el Data Access Component en un proyecto de biblioteca de clases en Visual Studio, y haré referencia al proyecto del DataHelper dentro de la solución.

Bien, comencemos por declarar la clase:

VB

Imports DataHelper.DBInfo

Imports DataHelper.DataHelper

Imports System.Data.SqlClient

 

Namespace DAC

    Partial Public Class TipoProducto

        Inherits SqlDataHelper

 

    End Class

End Namespace

C#

using DataHelper;

using DBInfo;

using System.Data;

using System.Data.SqlClient;

 

namespace DAC

{

    public partial class TipoProducto : SqlDataHelper

    {

       

    }

}

Declaramos la clase de preferencia con el mismo nombre que la tabla para ser congruentes con lo que tenemos en la aplicación y en la base de datos, nos será fácil identificar la clase a la que pertenece una tabla de la base de datos. Otra observación es que estoy creando un espacio de nombre específico denominado DAC, eso con la finalidad de identificar separa las clases de acceso a datos de las clases de negociación.

 Teniendo la declaración de la clase procederemos a lo obligatorio, primeramente identificaremos los campos de la tabla y sus tipos de datos:

IdTipoProducto, int, identity

ClaveTipoProducto, varchar(10)

Descripcion, varchar(150)

Procedemos a mapear los tipos al tipo de dato que le correspondería según el sistema de tipos del .Net Framework de la siguiente manera:

IdTipoProducto, int, identity à System.Int32 à VB: Integer ; C#: int

ClaveTipoProducto, varchar(10) à System.String à VB: String; C#: string

Descripcion, varchar(150) à System.String à VB: String; C#: string

Con esto podemos definir las propiedades para las cuales primeramente declararemos sus variables de apoyo:

VB:

Partial Public Class TipoProducto

    Inherits SqlDataHelper

 

    Private _IdTipoProducto As Integer

    Private _ClaveTipoProducto As String

    Private _Descripcion As String

    Private _Nuevo As Boolean

 

End Class

C#:

public partial class TipoProducto : SqlDataHelper

{

    private int tipoProducto;

    private string claveTipoProducto;

    private string descripcion;

    private bool nuevo;

}

Tenemos ya identificadas las propiedades, sin embargo antes de escribirlas deberemos crear los constructores, que debido a la herencia son obligatorios, bueno, al menos uno por la herencia, pero declararemos dos como teniendo la clase como sigue, inclusive las propiedades. Además tendremos en consideración una variable que no es propiamente un campo pero que indica algo muy importe que será lo que le dé cierta inteligencia a la clase para saber cómo comportarse al momento de salvar un registro, y quedaría más o menos como sigue:

VB:

Partial Public Class TipoProducto

    Inherits SqlDataHelper

 

    Private _IdTipoProducto As Integer

    Private _ClaveTipoProducto As String

    Private _Descripcion As String

    Private _Nuevo As Boolean

 

    Public Sub New(ByVal SqlSet As IDbSettings)

        MyBase.New(SqlSet)

 

    End Sub

 

    Public Sub New(ByVal SqlSet As IDbSettings, _

                   ByVal pIdTipoProducto As Integer)

        MyBase.New(SqlSet)

 

    End Sub

 

    Public ReadOnly Property IdTipoProducto() As Integer

        Get

            Return _IdTipoProducto

        End Get

    End Property

 

    Public Property ClaveTipoProducto() As String

        Get

            Return _ClaveTipoProducto

        End Get

        Set(ByVal value As String)

            _ClaveTipoProducto = value

        End Set

    End Property

 

    Public Property Descripcion() As String

        Get

            Return _Descripcion

        End Get

        Set(ByVal value As String)

            _Descripcion = value

        End Set

    End Property

 

    Public ReadOnly Property Nuevo() As Boolean

        Get

            Return _Nuevo

        End Get

    End Property

End Class

C#

public partial class TipoProducto : SqlDataHelper

{

    private int idTipoProducto;

    private string claveTipoProducto;

    private string descripcion;

    private bool nuevo;

 

    public TipoProducto(IDbSettings sqlSet)

        : base(sqlSet)

    {

 

    }

 

    public TipoProducto(IDbSettings sqlSet,

                        int pIdTipoProducto)

        : base(sqlSet)

    {

 

    }

 

    public int IdTipoProducto

    {

        get

        {

            return idTipoProducto;

        }

    }

    public string ClaveTipoProducto

    {

        get

        {

            return claveTipoProducto;

        }

        set

        {

            claveTipoProducto = value;

        }

    }

    public string Descripcion

    {

        get

        {

            return descripcion;

        }

        set

        {

            descripcion = value;

        }

    }

    public bool Nuevo

    {

        get

        {

            return nuevo;

        }

    }

}

 

Hasta aquí, no se pierdan, fíjense bien que solo agregué las propiedades y los constructores. Está bien, pero notaremos algunas particularidades.

Primeramente, la propiedad IdTipoProducto es de solo lectura. Esto es debido a que es en la tabla tiene ese comportamiento, es decir, un identity en una tabla de SQL Server es de solo lectura y se genera automáticamente de acuerdo a la configuración de incremento. Teniendo en cuenta eso, en nuestra clase pasará como una propiedad de solo lectura, asegurándonos de que no se modifique.

Seguido, veremos que tenemos un constructor adicional al constructor requerido, bien, eso lo detallaré en seguida, así que no desesperen.

Antes de utilizar la clase, debo inicializar mis variables a los valores predeterminados, o bien, inicializar las propiedades. Crearemos un método llamado InitializeVars(), mismo que invocaremos en cada constructor, como se muestra a continuación:

VB:

Public Sub New(ByVal SqlSet As IDbSettings)

    MyBase.New(SqlSet)

    InitializeVars()

End Sub

 

Public Sub New(ByVal SqlSet As IDbSettings, _

               ByVal pIdTipoProducto As Integer)

    MyBase.New(SqlSet)

    InitializeVars()

End Sub

 

Private Sub InitializeVars()

    _IdTipoProducto = -1

    _ClaveTipoProducto = String.Empty

    _Descripcion = String.Empty

    _Nuevo = True

End Sub

C#:

public TipoProducto(IDbSettings sqlSet)

    : base(sqlSet)

{

    InitializeVars();

}

 

public TipoProducto(IDbSettings sqlSet,

                    int pIdTipoProducto)

    : base(sqlSet)

{

    InitializeVars();

}

 

private void InitializeVars()

{

    idTipoProducto = -1;

    claveTipoProducto = string.Empty;

    descripcion = string.Empty;

    nuevo = true;

}

 

Con el método que agregamos, aseguramos que la clase tenga sus valores predeterminados. Podemos ver que de inicio  la clase tiene la variable nuevo asignado a true, esto es, que el estado del registro al momento de inicializar la clase es de uno nuevo. Sin embargo podemos construir la clase con información determinada en un registro de la base de datos, este registro estaría dado por el identificador, en nuestro caso, por IdTipoProducto, y es por eso que tenemos un constructor adicional que incluye este dato como parámetro.

Aquí viene un segundo método, el cual inicializa la clase respecto al identificador del registro. En resumen, estos dos métodos son parte básica de la clase de acceso a datos. Bien, veamos pues el método InitializeClass, que será el que inicializará la clase, tendrá un parámetro que será el que identifica al registro y será invocado solo desde el constructor que tiene los parámetros de identificación del registro. Este método hará uso de otro método llamado InitializeFields  que agrupa la inicialización de variables en base a un DataRow, esto será muy útil en ciertos procesos de inicialización masiva en una colección, y entenderán por qué al ver el método, veamos pues:

VB:

Protected Sub IntializeFields(ByVal dr As DataRow)

    If Not dr Is Nothing Then

        _IdTipoProducto = CInt(dr(“IdTipoProducto”))

        _ClaveTipoProducto = CStr(dr(“ClaveTipoProducto”))

        _Descripcion = CStr(dr(“Descripcion”))

        _Nuevo = False

    End If

End Sub

C#:

 

protected void InitializeFields(DataRow dr)

{

    if (dr != null)

    {

        idTipoProducto = (int)dr[“IdTipoProducto”];

        claveTipoProducto = (string)dr[“ClaveTipoProducto”];

        descripcion = (string)dr[“Descripcion”];

        nuevo = false;

    }

}

Noten que la variable nuevo se ha asignado a false, son lo que indicamos al DAC que la recuperación del registro fue exitosa y tenemos datos para utilizar. Esto nos servirá más adelante cuando se tenga que decidir si el registro que está almacenado en el DAC es nuevo (no existe aún en la base de datos) o no lo es (porque ya existe en la base de datos)

Hey, sí, ya se que me faltó el método InitializeClass… pero esperen, tenemos que falta agregar ya los accesorios que nos harán platicar con la base de datos. Hasta este momento hemos escrito todo el código de manera que no se ha hablado con la base de datos, ahora bien, para poder platicar con la base de datos, debemos establecer el transporte de esa plática por medio de un SqlCommand, además, debemos elegir de qué manera hablaremos con la base de datos, si en sus términos o en los nuestros. En sus términos me refiero a tener uno o varios Stored Procedures en la base de datos para ejecutar instrucciones de nuestro componente de acceso a datos. En nuestros términos me refiero a que las instrucciones las escribimos en el código y luego las mandamos a ejecutar directamente a la base de datos.

Bien, pues el  modelo de la arquitectura no restringe en qué términos debemos hablar con la base de datos, sin embargo, sí recomienda que lo hagamos en términos de la base de datos, o sea, con Stored Procedure (SP), ya que delegamos parte del trabajo al motor de la base de datos aligerándole un poco las tareas a nuestro componente. Esta razón es la que me convence más, aún cuando no habría diferencia entre hacerlo desde una instrucción o desde un SP, pero habrá otro tipo de procesamiento que será mejor tenerlo del lado de la base de datos y con un SP tendremos listo el camino.

Sobre el SP, bien, pues tomaré de base un formato de uso frecuente, que muchos utilizan de forma habitual y es un SP con opciones diversas para cada operación, mismas que estarán correspondidas. Aquí muestro cómo sería nuestro SP:

T-SQL:

USE [EjemploDAC]

GO

SET ANSI_NULLS ON

GO

SET QUOTED_IDENTIFIER ON

GO

 

 CREATE PROCEDURE [dbo].[SP_TipoProducto]

 

 – Variable de opción del SP

 @SP_OPCION             TINYINT,         

/* Número de Opciones:

1 – INSERTAR

2 – MODIFICAR POR CLAVE

3 – CANCELAR POR CLAVE

4 – Devuelve el Registro por ID

5 – Devuelve TODO lo de la tabla

*/

–Inicialziación de variables

 @IdTipoProducto  int= 0 OUTPUT,

 @ClaveTipoProducto varchar(20)  = ‘Sin Dato’,

 @Descripcion           varchar(150) = ‘Sin Dato’

AS

– Insertar un registro

IF (@SP_OPCION = 1) BEGIN

    INSERT INTO TipoProducto

          (ClaveTipoProducto, Descripcion)

    VALUES

          (@ClaveTipoProducto, @Descripcion )

 

     SET @IdTipoProducto = @@IDENTITY

END

– Actualizar un registro

IF (@SP_OPCION = 2) BEGIN 

 

    UPDATE

          TipoProducto

    SET

          ClaveTipoProducto = @ClaveTipoProducto,

          Descripcion = @Descripcion

    WHERE

          IdTipoProducto = @IdTipoProducto

END

–Eliminar el registro

IF (@SP_OPCION = 3) BEGIN 

 

    DELETE

          TipoProducto           

    WHERE

          IdTipoProducto = @IdTipoProducto

END

–Consulta el registro por ID

IF (@SP_OPCION = 4) BEGIN

 

    SELECT

          ClaveTipoProducto,

          Descripcion, IdTipoProducto

    FROM

          TipoProducto            

    WHERE

              IdTipoProducto = @IdTipoProducto

END

–Consulta completa

IF (@SP_OPCION = 5) BEGIN 

 

    SELECT

          ClaveTipoProducto,

          Descripcion, IdTipoProducto

    FROM

          TipoProducto           

END

 

Una vez incluido el SP en la base de datos, para la tabla correspondiente, podremos hacer uso de sus distintas opciones asignando la variable @SP_OPCION. Necesitaremos una variable del tipo SqlCommand a nivel de clase para poder manipular el SP.

Procedemos a declarar la variable respectiva y a incluir su inicialización en el método InitializeVars:

VB:

Private cmd As SqlCommand

 

Private Sub InitializeVars()

    _IdTipoProducto = -1

    _ClaveTipoProducto = String.Empty

    _Descripcion = String.Empty

    _Nuevo = True

    cmd = New SqlCommand(“SP_TipoProducto”)

    cmd.CommandType = CommandType.StoredProcedure

End Sub

C#:

private SqlCommand cmd;

 

private void InitializeVars()

{

    idTipoProducto = -1;

    claveTipoProducto = string.Empty;

    descripcion = string.Empty;

    nuevo = true;

    cmd = new SqlCommand(“SP_TipoProducto”);

    cmd.CommandType = CommandType.StoredProcedure;

}

Tengan en cuenta que solo estamos agregando la variable y la parte correspondiente a la inicialización de la variable cmd. No se preocupen, pondré la clase completa al final. Bien, ahora que ya contamos con esta variable y con el SP, podremos inicializar la clase a partir de un identificador:

VB:

Private Sub InitializeClass( _

        ByVal pIdTipoProducto As Integer)

    Dim dr As DataRow

    cmd.Parameters.Clear()

    cmd.Parameters.Add(“@SP_OPCION”, _

        SqlDbType.TinyInt).Value = 4

    cmd.Parameters.Add(“@IdTipoProducto”, _

        SqlDbType.Int).Value = pIdTipoProducto

    dr = GetRecord(cmd)

    IntializeFields(dr)

End Sub

C#:

private void InitializeClass(int pIdTipoProducto)

{

    DataRow dr;

    cmd.Parameters.Clear();

    cmd.Parameters.Add(“@SP_OPCION”,

        SqlDbType.TinyInt).Value = 4;

    cmd.Parameters.Add(“@IdTipoProducto”,

        SqlDbType.Int).Value = pIdTipoProducto;

    dr = base.GetRecord(cmd);

    InitializeFields(dr);

}

Declaramos un DataRow para poder pasar el parámetro al método InitializeFields que está descrito anteriormente, además, limpiamos los parámetros del  SqlCommand para no llenar de basurita la colección de parámetros. Asignamos dos de los parámetros del SP, el primero siempre se usa, el segundo solo porque en la opción 4 del SP se utiliza. Seguido invocamos un método del DataHelper para asignar su valor al DataRow y al final invocamos al método que inicializará la clase.

Con esto llegamos hasta la inicialización del componente de acceso a datos, nos estará faltando la segunda pare, y la más emocionante pues es la construcción de los métodos de Salvar y Eliminar.

Bien, pues quedo pendiente para la próxima donde terminaré este ejemplo.

Referencias:

Parte 2: http://msmvps.com/blogs/otelis/archive/2010/05/12/arquitectura-definici-243-n-de-un-data-access-component-con-un-ejemplo-parte-2.aspx

Parte 3: http://msmvps.com/blogs/otelis/archive/2010/05/14/arquitectura-definici-243-n-de-un-data-access-component-con-un-ejemplo-parte-3.aspx

Saludos…

Octavio Telis