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"))
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"];
}
}
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("@IdContrato",
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.
Saludos…
Octavio Telis