004.68-F363m-Capitulo IV

Anuncio
Maestría en Informática Aplicada a Redes
IV. SOLUCIÓN PROPUESTA
El presente capitulo, describe la conceptualización, elaboración y construcción de la
solución propuesta. De esta forma que se detallan los requerimientos base con los
que se inicio la conceptualización y diseño de la solución, los criterios y estimaciones
de diseño y finalmente se muestra la implementación del diseño utilizando código C#.
La propuesta se plantea, presentando inicialmente el modelado general de la
solución describiendo elementos de arquitectura considerados y las características
generales propuestas. Posteriormente se presenta la funcionalidad básica que debe
de implementar aislando en secciones cada funcionalidad con sus requerimientos,
sus criterios de diseño y su implementación, con el fin de que sea mas sencillo
detallar la construcción de cada sección especifica de la solución propuesta.
4.1
Modelado de la solución
4.1.1 Consideraciones Iniciales
Dado que nuestra solución pretende reforzar y promover el uso de un diseño y
construcción completamente orientado a objetos, esto requiere la utilización de una
arquitectura multicapas como las mostrada en el siguiente diagrama.
Presentación
<<Usa>>
Negocio
<<Usa>>
<<Usa>>
Acceso a Datos
<<Crea>>
Dominio
Para el diseño del core del sistema se construirá un modelo de dominio basados en
el patrón Domain Value Object, donde en ves de diseñar tablas o un modelo ER, se
Página 97 de 190
Maestría en Informática Aplicada a Redes
diseñaran
clases
que
encapsulen
datos
y
comportamiento
siguiendo
las
características de la orientación a objetos.
El motor de persistencia a construir implementará funcionalidad de la capa de
Acceso a Datos, por lo que es en este nivel donde definirá el ambiente, estándares y
características bajo los cuales trabajara la propuesta.
Debido que existen una serie de patrones de diseño disponibles para el Acceso a
Datos, se ha decido tomar el patrón utilizado en la plataforma J2EE denominado
DAO (Data Access Object)4. Este patrón proporcionara los siguientes beneficios a
nuestro diseño.
•
DAO define una relación uno a uno entre clases de dominio y de acceso a
datos
•
Cada clase de dominio tendrá asociada una clase de DAO de acceso a datos
lo que permitirá no mezclar operaciones entre clases e identificar rápidamente
la clase responsable de cada entidad.
•
El uso de DAO proporciona una alta cohesión ya que todos los métodos
transaccionales de una clase están relacionados a una sola clase de dominio
y el acoplamiento entre clases DAO es bastante bajo.5
•
El uso de DAO permitirá una alta reusabilidad de las clases implementadas.
DAOMaster
Dado que por cada clase de dominio
tendremos una clase de acceso a
datos, podemos considerar el diseño
de una clase maestra que contenga
DAOCliente
DAOFactura
cada una de las clases DAO de la
<<Crea>>
<<Crea>>
aplicación. Esto se muestra en el
Cliente
DAOItem
DAOProducto
la funcionalidad genérica del motor
de persistencia y sea heredado a
Factura
<<Crea>>
Item
<<Crea>>
Producto
diagrama adjunto.
4
5
Para mas información sobre el patrón DAO consultar la sección 3.2.9
Para mas información sobre Acoplamiento y Cohesión remitirse a la sección 3.2.2
Página 98 de 190
Maestría en Informática Aplicada a Redes
4.1.2 Namespaces y Assemblies.
La funcionalidad principal del motor de persistencia será encapsulada en una clase a
la que se denominará DAOMaster la cual deberá ser heredada, por lo que se definirá
de tipo abstracta. Adicionalmente esta clase dependerá de los servicios de otras
clases encargadas de crear conexiones y otros tipos de objetos.
Toda esta
funcionalidad se definirá y agrupará dentro de un solo namespace el cual será
denominado DAOMasterLibrary con el fin de que solo sea incorporado en la lógica
de las capa de acceso datos de las aplicaciones que utilicen esta herramienta.
Así a la hora de diseñar la arquitectura de una aplicación la lógica de acceso a datos
será heredada de este namespace, esto como se muestra en el siguiente diagrama:
DAOMasterLibrary
Negocio
<<Usa>>
<<Usa>>
Acceso a Datos
<<Crea>>
Dominio
Finalmente la implementación del motor de persistencia, estará contenida en un
solo
Assembly
o
librería
de
clases
DLL
la
cual
será
denominada
DAOMasterLibrary.dll . Este podrá ser incorporado en cualquier proyecto de
desarrollo, simplemente al agregar una referencia a esta librería y seguir el esquema
propuesto.
Página 99 de 190
Maestría en Informática Aplicada a Redes
Finalmente el motor de persistencia debe de implementar ciertas características
como la Sincronización de la Estructura del Modelo de Dominio con el modelo ER
donde se persistirán los datos,
generación automática de las sentencias de
inserción, actualización y eliminación, carga de objetos,
implementar las
características anteriores independientes del sistema RDBMS o contenedor usado,
parametrización, etc. Todas estas característicos son explicadas en la secciones
siguientes del presente capitulo.
4.2
Sincronización Estructura ER y Modelo de Dominio
4.2.1 Planteamiento del Requerimiento
Dado que la persistencia de datos es una característica que debemos de
implementar sobre el modelo de dominio, es necesario que la funcionalidad del motor
de persistencia a crear contenga un mecanismo a través del cual se mantenga una
relación bien sincronizada entre el modelo de dominio y su respectivo equivalente en
el RDBMS. Esta sincronización debe de garantizar que cada instancia de una clase
del Dominio pueda ser persistida como una o varias filas en una o varias tablas
especificas, de la misma forma se debe de garantizar que la filas de las tablas
puedan ser cargadas dentro de objetos en tiempo de ejecución.
Las operaciones antes mencionadas requieren que exista una forma de indicar al
Motor una relación uno a uno entre tablas y clases así como también relaciones uno
a uno entre columnas de una tabla y las propiedades de una clase.
Se debe de considerar como parte del requerimiento que esta operación de mapeo
requiera el menor tiempo posible al desarrollador, de tal forma que no le rastrase o le
obligue a realizar complejos o detallados mapeos entre cada propiedad de una tabla
y las propiedades de una clase.
4.2.2 Diseño de la Solución
Motores de persistencia como Hibernate o NHIbernate utilizan un archivo XML para
definir el mapeo entre clases y tablas. Para nuestro caso se ha considerado
Página 100 de 190
Maestría en Informática Aplicada a Redes
automatizar totalmente dicho mapeo, sin embargo esta funcionalidad supone que el
desarrollador utilizará una herramienta CASE que le permita modelar un sistema en
un diagrama de clases, es decir construir un modelo de dominio y su respectivo
código C# y a partir de este generar el DDL para ser ejecutado en el RDBMS y
construir de esa forma el ER a partir del diagrama de clases. Este mecanismo
solucionara el problema de mantener sincronizado ambos modelos, asignando dicha
responsabilidad a la herramienta CASE.
4.2.2.1
Diseñando la Clase de Dominio
Por ejemplo para el modelamiento de los usuarios de un sistema, se diseña la
siguiente clase:
Usuario
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
<<Property>>
<<Property>>
<<Property>>
<<Property>>
<<Property>>
<<Property>>
<<Property>>
<<PropertyImplementation>>
<<PropertyImplementation>>
<<PropertyImplementation>>
<<PropertyImplementation>>
<<PropertyImplementation>>
<<PropertyImplementation>>
<<PropertyImplementation>>
<<Setter>>
<<Getter>>
<<Setter>>
<<Getter>>
<<Setter>>
<<Getter>>
<<Setter>>
<<Getter>>
<<Setter>>
<<Getter>>
<<Setter>>
<<Getter>>
<<Setter>>
<<Getter>>
usuarioID
usuarioNombres
usuarioApellidos
usuarioEmail
usuarioCelular
usuarioPassword
fechaActualizacion
_usuarioID
_usuarioNombres
_usuarioApellidos
_usuarioEmail
_usuarioCelular
_usuarioPassword
_fechaActualizacion
: string
: string
: string
: string
: string
: string
: Datetime
: string
: string
: string
: string
: string
: string
: Datetime
Usuario (string sUsuarioCuenta, string sUsuarioNombre, string sUsuarioPassword)
: void
: int
encriptarClave ()
validarClave (string sPassword)
: bool
cambiarPassword (string sPasswordAnterior, string sNuevoPassword)
: int
set_usuarioID (string value)
: void
get_usuarioID ()
: string
set_usuarioNombres (string value)
: void
get_usuarioNombres ()
: string
set_usuarioApellidos (string value)
: void
get_usuarioApellidos ()
: string
set_usuarioEmail (string value)
: void
get_usuarioEmail ()
: string
: void
set_usuarioCelular (string value)
get_usuarioCelular ()
: string
set_usuarioPassword (string value)
: void
get_usuarioPassword ()
: string
set_fechaActualizacion (Datetime value)
: void
get_fechaActualizacion ()
: Datetime
Página 101 de 190
Maestría en Informática Aplicada a Redes
Se puede notar de esta clase que tiene 7 propiedades las cuales son privadas, es
decir no pueden ser acezadas directamente, sino solamente a través de la
propiedades (getter y setter) las cuales son métodos con el mismo nombre que los
atributos de la clase, esta es la implementación del Data Hidding y de la
Encapsulación de objeto.
Adicionalmente se han agregado las siguientes reglas de negocio a este modelo:
•
No es posible crear una nueva instancia de un objeto Usuario si no se cuenta
con los datos obligatorios UsuarioID, UsuarioNombre y UsuarioClave. Estos
son parámetros obligatorios del Constructor de la Clase.
•
No es posible cambiar las propiedades UsuarioID, UsuarioNombre e
UsuacioClave esta ultima solo puede ser cambiada a través del método
cambiarPassword. Debido a esto los Setter de la propiedades UsuarioID,
UsuarioNombre y UsuarioClave, si bien existen son privados por lo que no
pueden ser invocados externamente.
•
La clase cuenta con un método privado encriptarClave el cual se encarga de
codificar la clave del usuario de tal forma que este encriptada al momento que
el usuario sea persistido
•
La clase Usuario cuenta con el método validarClave la cual recibe una cadena
de texto y devuelve un valor verdadero si es la clave asignada al usuario o
falso si no es así
Estas propiedades y métodos que mantienen un alto grado de cohesión permitirán
dar a esta clase la responsabilidad única de representar a los usuarios manteniendo
un bajo grado de acoplamiento de tal forma que el objeto pueda ser reutilizable.
El código generado por el case para esta clase, es el siguiente:
public class Usuario {
private string _usuarioID;
private string _usuarioNombres;
private string _usuarioApellidos;
private string _usuarioEmail;
Página 102 de 190
Maestría en Informática Aplicada a Redes
private string _usuarioCelular;
private string _usuarioPassword;
private Datetime _fechaActualizacion;
public sealed void Usuario(string sUsuarioCuenta, string sUsuarioNombre, string
sUsuarioPassword) {
// TODO: implement
}
public sealed int encriptarClave() {
// TODO: implement
return 0;
}
public sealed bool validarClave(ref string sPassword) {
// TODO: implement
return false;
}
public sealed int cambiarPassword(ref string sPasswordAnterior, ref string
sNuevoPassword) {
// TODO: implement
return 0; }
private string usuarioID
{
get {
return _usuarioID;
}
Set {
if (this._usuarioID != value)
this._usuarioID = value;
}
Página 103 de 190
Maestría en Informática Aplicada a Redes
}
private string usuarioNombres
{
get {
return _usuarioNombres;
}
Set {
if (this._usuarioNombres != value)
this._usuarioNombres = value;
}
}
private string usuarioApellidos {
get {
return _usuarioApellidos;
}
Set {
if (this._usuarioApellidos != value)
this._usuarioApellidos = value;
}
}
private string usuarioEmail
{
get {
return _usuarioEmail;
}
Set
{
if (this._usuarioEmail != value)
this._usuarioEmail = value;
}
}
private string usuarioCelular
{
Página 104 de 190
Maestría en Informática Aplicada a Redes
get {
return _usuarioCelular;
}
Set {
if (this._usuarioCelular != value)
this._usuarioCelular = value;
}
}
private string usuarioPassword
get
{
{
return _usuarioPassword;
}
Set {
if (this._usuarioPassword != value)
this._usuarioPassword = value;
}
}
Private Datetime fechaActualizacion {
Get {
return _fechaActualizacion;
}
Set {
if (this._fechaActualizacion != value)
this._fechaActualizacion = value;
}
}
}
Página 105 de 190
Maestría en Informática Aplicada a Redes
4.2.2.2
Diseñando el Modelo Físico en el RDBMS
Habiendo finalizado el modelo de dominio, podemos utilizar el CASE para que nos
genere el modelo Físico orientado a un sistema RDBMS especifico, para nuestro
caso, generamos el modelo físico especificando Microsoft SQL Server como RDBMS
y el CASE nos genero la siguiente tabla
Usuario
usuarioID
usuarioNombres
usuarioApellidos
usuarioEmail
usuarioCelular
usuarioPassword
fechaActualizacion
varchar(50)
varchar(100)
varchar(100)
varchar(100n)
varchar(8)
varchar(100)
Datetime
<pk>
El DDL de dicha clase es el siguiente
create table Usuario (
usuarioID
varchar(50)
not null,
usuarioNombres
varchar(100)
not null,
usuarioApellidos varchar(100)
null,
usuarioEmail
varchar(100n)
null,
usuarioCelular
varchar(8)
null,
usuarioPassword
varchar(100)
not null,
fechaActualizacion Datetime
not null,
constraint PK_USUARIO primary key (usuarioID)
)
Este puede ya ser ejecutado contra la base de datos para definir el ER de nuestro
sistema.
4.2.3 Construcción de la Solución.
Los siguientes, son mas que construcciones dentro del prototipo, son bien
lineamientos que deberán seguirse al construir la aplicación particular para garantizar
que el motor de persistencia identifique la relación entre clases y tablas.
Se deberá construir un namespace denominado Dominio dentro del cual deberán de
declararse todas las clases de dominio que formen el aplicativo. Entonces el código
generado por el case quedaría de la siguiente manera:
Página 106 de 190
Maestría en Informática Aplicada a Redes
Namespace Dominio {
public class Usuario {
private string _usuarioID;
private string _usuarioNombres;
private string _usuarioApellidos;
private string _usuarioEmail;
private string _usuarioCelular;
private string _usuarioPassword;
private Datetime _fechaActualizacion;…
…
}
}
Para cada clase de dominio se deberá crear una clase DAO en cuyo constructor se
declare a través de la ejecución del método setDoName el nombre de la tabla del
ER al cual esta mapeada dicha clase. Por nomenclatura el nombre de la clase
deberá ser la cadena “DAO” mas el nombre de la clase de dominio. Estas clases
DAO deberán ser definidas dentro de un namespace denominado “AccedoDatos” y
cada clase deberá ser heredada de la clase DAOMaster.
Para la clase de Dominio Usuario, la clase de acceso a datos quedaría de la
siguiente forma:
using System;
using System.Data;
using DAOMasterLibrary;
using Dominio;
namespace AccesoDatos
{
class DAOUsuario : DAOMaster
{
public DAOUsuario()
Página 107 de 190
Maestría en Informática Aplicada a Redes
{
this.setDOName("usuario");
}
}
}
4.3
Generación de operaciones transaccionales
4.3.1 Planteamiento del Requerimiento
El sistema a implementar debe de generar las operaciones de inserción,
actualización y eliminación en contra del RDBMS de forma automática, recibiendo
como insumo únicamente una instancia del objeto a procesar o su respectivo ID.
Dado que el motor de persistencia trabaja en la capa de acceso a datos, será a este
nivel donde deberán de estar disponibles métodos para adicionar, actualizar y
eliminar objetos. Los cuales recibirán una instancia y generarán las operaciones
pertinentes de la forma mas eficiente posible. Esto significa para el caso de la
actualización por ejemplo, solo actualizar las propiedades que hayan cambiado y no
toda la fila.
4.3.2 Diseño de la Solución
Dado el requerimiento anterior, se han diseñado tres métodos concretos los cuales
se describen a continuación:
1
2
Método
add
Update
Parámetros
Domain Object
Domain Object
3
Delete
Domain Object
Descripción
Adiciona el objeto
Actualiza las propiedades del objeto que
han cambiado
Elimina el Objeto
Página 108 de 190
Maestría en Informática Aplicada a Redes
4.3.2.1
Método para la inserción de registros.
El método Add que será implementado en la Capa de acceso a Datos para ser usado
en la capa de negocios, tendrá la responsabilidad de recibir una instancia de un
Domain Object y convertirlo a un registro de la correspondiente tabla mapeada,
generando la correspondiente sentencia insert. Para desarrollar esta tarea dicho
método, se auxiliara de una serie de métodos privados siguiendo la secuencia que se
muestra en el siguiente diagrama:
setdoName
add
getSchema
getPropierties
saveDS
getID
RDBMS
Setear Nombre Objeto
Solicitar Esquema
Devuelve Esquema Tabla
Crear Row a paritr de esquema
Envio de objeto y DataRow vacío
DataRow poblado con propiedades de objeto
Adicionar Datarow a Dataset
Envio DataSet
envio de sentencia Insert generada
Exito o Exepción
Resultado Exito o Exepción
Solicitar Pk de registro insertado
PK insertado
Los métodos utilizados en la secuencia anterior se describen a continuación:
Método
SetdoName
getSchema
Descripción
Método Setter que establece el nombre de la clase a procesar
Obtiene el esquema de la tabla identificada por el nombre del
objeto
Página 109 de 190
Maestría en Informática Aplicada a Redes
getPropierties Recibe la instancia del objeto a persistir y un Datarow vació con la
estructura de la tabla, barre la instancia y asigna los valores de
cada propiedad a su respeciva columna en el datarow, devolviendo
el datarow poblado
saveDS
Toma un Dataset y genera una sentencia insert para el datarow
contenido
4.3.2.2
Método para la actualización de registros.
El método update a implementar en la Capa de Acceso a Datos. Recibirá la
referencia del objeto de dominio a actualizar y tendrá la responsabilidad de generar
la sentencia update basado en los primary keys del registro actualizando únicamente
las columnas que han cambiado.
Para realizar esta actividad el método update, se Valera de la ayuda de una serie de
métodos privados, siguiendo la secuencia del diagrama siguiente:
Página 110 de 190
Maestría en Informática Aplicada a Redes
update
setDoName
getSchema
getPropierties
saveDS
getStrWhere
RDBMS
Setear Nombre Objeto
Generar Where para registro a partir de primaries Keys
Sentencia Where
ejecutar select con el where construido
DataRow poblado
Solicitar Esquema
Devuelve Esquema Tabla
Crear DataSet a paritr de esquema
Asignar Datarow poblado a DataSet Creado
Envio de objeto y DataRow vacío
DataRow poblado con propiedades de objeto
Asignar nuevo valores a existentes en DataRow
Envio DataSet
Exito o Exepción
envio de sentencia update generada
Resultado Exito o Exepción
Los métodos utilizados en la secuencia anterior se describen a continuación:
Método
SetdoName
getSchema
Descripción
Método Setter que establece el nombre de la clase a procesar
Obtiene el esquema de la tabla identificada por el nombre del
objeto
getPropierties Recibe la instancia del objeto a persistir y un Datarow vació con la
estructura de la tabla, barre la instancia y asigna los valores de
cada propiedad a su respectiva columna en el datarow,
devolviendo el datarow poblado
saveDS
Toma un Dataset y genera una sentencia update para el datarow
contenido
getWhere
Genera un string que servirá como filtro where para un Quero, esta
sentencia esta formada por las columnas definidas como Primary
Página 111 de 190
Maestría en Informática Aplicada a Redes
Keys para la tabla mapeada así como el valor respectivo que hace
único al registro
4.3.2.3
Método para la eliminación de registros
El método delete que implementara el motor de persistencia, recibirá un objeto de
dominio y partir de esto deberá de generar la sentencia delete respectiva filtrada por
las columnas que forman la primary Key y sus valores respectivos. Al igual que los
casos de adición y actualización el método se auxiliara de métodos privados,
siguiendo la secuencia mostrada en el siguiente diagrama:
delete
setdoName
getStrWhere
execSQL
RDBMS
Setear Nombre Objeto
Contruir Where con Primary keys
String para where
Envio de sentencia delete con where creado
Exito o Exepcion
Ejecutar Delete
Exito o Exepcion
Los métodos utilizados en la secuencia anterior se describen a continuación:
Método
Descripción
SetdoName Método Setter que establece el nombre de la clase a procesar
getStrWhere Genera un string que servirá como filtro where para un Quero, esta
sentencia esta formada por las columnas definidas como Primary
Keys para la tabla mapeada así como el valor respectivo que hace
único al registro
execSQL
Ejecuta una sentencia SQL
Página 112 de 190
Maestría en Informática Aplicada a Redes
4.3.3 Construcción de la Solución
En esta sección se muestra el código de los métodos implementados para soportar
las operaciones de adición, actualización y eliminación siguiendo la lógica y
secuencias planteados en el diseño de los mismos.
4.3.3.1
Método Add
Este método público recibe un Domain Object e intenta persistirlo en Data Source
especificado. El código de dicho método es el siguiente:
// Salva el Domain Object en el Datasource
public long add(object aDO) {
try {
setDOName(aDO.GetType().Name.ToString());
DataSet oDS = new DataSet() ;
oDS.ReadXmlSchema(getSchema());
DataRow oRow = oDS.Tables[0].NewRow();
getPropiedades(oRow,aDO);
oDS.Tables[0].Rows.Add(oRow);
saveDS(oDS);
} catch (Exception ex) {
throw new Exception("Error Insertando objeto " + this._doName + " / " +
ex.Message);
}
return getID();
}
Puede observarse que debido a que el parámetro es un objeto de tipo Domain Object
que puede ser en si cualquier objeto, es necesario la utilización de “Late Binding” al
momento de definir el tipo de dato del argumento recibido.
El método ADD utiliza se vale de una serie de modos como se mostró en la
secuencia del diseño, el código de estos métodos es mostrado a continuación:
// Devuelve el Schema en formato XML StrinReader de la estructura de persistencia
del Domain Object actual
private System.IO.StringReader getSchema() {
Página 113 de 190
Maestría en Informática Aplicada a Redes
string sXML = this.getDsSchema().GetXmlSchema();
return new System.IO.StringReader(sXML);
}
//Función privada que ejecuta y devuelve el Schema del Objeto mapeado
private DataSet getDsSchema(){
if (dsSchema != null) return dsSchema;
else
{
DataSet ds;
DbCommand oCommand;
DbDataAdapter oAdapter;
string SqlString = "select * from " + this._doName;
try
{
if (setConnection())
{
ds = new DataSet();
oCommand = _oCon.CreateCommand();
oAdapter = dbFactory.CreateDataAdapter();
oCommand.Connection = _oCon;
oCommand.CommandType = CommandType.Text;
oCommand.CommandText = SqlString;
oAdapter.SelectCommand = oCommand;
oAdapter.FillSchema(ds, SchemaType.Mapped);
dsSchema = ds;
}
else
{
return null;
}
}
catch (Exception ex)
{
throw ex;
}
return ds;
}
}
// Obtiene las propiedades de un Domain Object asignandolas a un DataRow a
traves de invocaciones dinamicas a los Getter
private void getPropiedades(DataRow aRow, object aDO){
PropertyInfo[] oProperties;
string sPropertyName="";
Página 114 de 190
Maestría en Informática Aplicada a Redes
MethodInfo oMetodo;
try {
oProperties =
aDO.GetType().GetProperties(BindingFlags.NonPublic|BindingFlags.Instance|Binding
Flags.Static|BindingFlags.Public);
foreach(PropertyInfo oProperty in oProperties) {
sPropertyName = oProperty.Name.ToString();
oMetodo = oProperty.GetGetMethod(true);
switch (oMetodo.ReturnType.ToString()) {
case "System.String":
aRow[sPropertyName] = (string)oMetodo.Invoke(aDO, null);
break;
case "System.DateTime":
aRow[sPropertyName] = (DateTime)oMetodo.Invoke(aDO, null);
break;
case "System.Int16":
aRow[sPropertyName] = (Int16)oMetodo.Invoke(aDO, null);
break;
case "System.Int32":
aRow[sPropertyName] = (Int32)oMetodo.Invoke(aDO, null);
break;
case "System.Int64":
aRow[sPropertyName] = (Int64)oMetodo.Invoke(aDO, null);
break;
case "System.Decimal":
aRow[sPropertyName] = (Decimal)oMetodo.Invoke(aDO, null);
break;
case "System.Double":
aRow[sPropertyName] = (Double)oMetodo.Invoke(aDO, null);
break;
case "System.Boolean":
aRow[sPropertyName] = (Boolean)oMetodo.Invoke(aDO, null);
break;
case "System.Char":
aRow[sPropertyName] = (Char)oMetodo.Invoke(aDO, null);
break;
}
}
} catch(Exception ex) {
throw new Exception("Error accesando la propiedad " + sPropertyName + " / " +
ex.Message);
}
}
// Objetivo: Actualizar el Dataset en el DataSource
Página 115 de 190
Maestría en Informática Aplicada a Redes
private int saveDS(DataSet ds)
{
string sSQL = "select * from " + this._doName;
DbDataAdapter oAdapter;
DbCommandBuilder oCBuilder;
int iResult = 0;
if (setConnection())
{
try
{
oAdapter = dbFactory.CreateDataAdapter();
oAdapter.SelectCommand.CommandText = sSQL;
oAdapter.SelectCommand.Connection = this._oCon;
oCBuilder = dbFactory.CreateCommandBuilder();
oCBuilder.DataAdapter = oAdapter;
iResult = oAdapter.Update(ds);
oAdapter.Dispose();
oAdapter = null;
}
catch (Exception ex)
{
throw new Exception("Error actualizando objeto " + this._doName + " / " +
ex.Message);
}
}
return iResult;
}
4.3.3.2
Método Update.
Este método público recibe un Domain Object e intenta actualizarlo en el Data
Source especificado. El código de dicho método es el siguiente:
//Actualizar el Domain Object en el Datasource
public bool update(object aDO) {
this.setDOName(aDO.GetType().Name.ToString());
try
{
string sSQL = "select * from " + this._doName + " " + this.getWhere(aDO);
Página 116 de 190
Maestría en Informática Aplicada a Redes
DataSet oDS, fDS;
fDS = new DataSet();
fDS.ReadXmlSchema(getSchema());
oDS = getDS(sSQL);
DataRow oRow = oDS.Tables[0].Rows[0];
DataRow fRow = fDS.Tables[0].NewRow();
foreach(DataColumn oColumn in oDS.Tables[0].Columns) {
fRow[oColumn.ColumnName.ToString()] =
oRow[oColumn.ColumnName.ToString()];
}
fDS.Tables[0].Rows.Add(fRow);
fDS.AcceptChanges();
getPropiedades(fRow, aDO);
saveDS(fDS,true,aDO);
} catch(Exception ex) {
throw new Exception("Error actualizando objeto " + this._doName + " / " +
ex.Message);
}
return true;
}
El método actualizar se vale de los mismos métodos que Add, es decir los métodos
getSchema(), readXMLSchema(),getPropiedades() y saveDS() incorpora además el
método getDS() y getWhere() de los cuales se muestra el código a continuación:
//Función protected que ejecuta y devuelve un DataSet
protected DataSet getDS(string SqlString){
DataSet ds;
DbCommand oCommand = _oCon.CreateCommand();
DbDataAdapter oAdapter = dbFactory.CreateDataAdapter();
try {
if(setConnection()) {
ds = new DataSet() ;
oCommand.Connection = _oCon;
oCommand.CommandType = CommandType.Text;
oCommand.CommandText = SqlString;
oAdapter.SelectCommand = oCommand;
oAdapter.Fill(ds);
} else {
return null;
}
} catch(Exception ex) {
throw ex;
}
Página 117 de 190
Maestría en Informática Aplicada a Redes
return ds;
}
// Generar la Sentencia where correspondiente usando los primary key y sus valores
private string getWhere(object aDO)
{
DataColumn[] colArr;
string sWhere = "", sColumn, sAND, sType;
DataSet ds;
DataRow fila;
int ikeys;
try
{
ds = getDsSchema();
fila = ds.Tables[0].NewRow();
getPropiedades(fila,aDO);
colArr = ds.Tables[0].PrimaryKey;
ikeys = colArr.Length;
sColumn = "";
sWhere = " where ";
if (ikeys == 0) throw new Exception("Objeto " + _doName + " no tiene definido
Primary Key en el ER, imposible continuar");
for (int i = 1; i < ikeys; i++) {
sAND = (i>0)? " AND ":"";
sColumn = colArr[i].ColumnName;
sType = colArr[i].DataType.Name.ToString();
if(sType.CompareTo("String")==0||sType.CompareTo("Char")==0) {
sWhere += sAND + sColumn + " = '" + fila[sColumn] + "'";
} else {
sWhere += sAND + sColumn + " = " + Convert.ToString(fila[sColumn]) + " ";
}
}
}
catch (Exception ex) {
throw new Exception("Error obteniendo Primary Keys / " + ex.Message);
}
return sWhere;
}
4.3.3.3
Método Delete
Este método público recibe un Domain Object e intenta eliminarlo del Data Source
especificado. El código de dicho método es el siguiente:
Página 118 de 190
Maestría en Informática Aplicada a Redes
// Elimina el DVO en el Datasource
public bool delete(object aDO) {
try {
setDOName(aDO.GetType().Name.ToString());
string sSQL = "delete from " + this._doName + " " + getWhere(aDO);
execSQL(sSQL);
} catch(Exception ex) {
throw new Exception("Error eliminando objeto " + this._doName + " / " +
ex.Message);
}
return true;
}
Este método utiliza el mismo método getWhere utilizado en el Update, incorpora
además un nuevo método de tipo protected denominado execSQL. El código de este
método se muestra a continuación:
// Ejecutar un SQL
protected bool execSQL(string sSQL) {
DbCommand oCommand;
if(setConnection()) {
oCommand = dbFactory.CreateCommand();
try {
oCommand.CommandText = sSQL;
oCommand.Connection = this._oCon ;
oCommand.ExecuteNonQuery();
} catch(Exception ex) {
throw ex;
}finally {
oCommand.Dispose();
oCommand = null;
}
} else return false;
return true;
}
Página 119 de 190
Maestría en Informática Aplicada a Redes
4.4
Construcción y Cargas de los Objetos
4.4.1 Planteamiento del Requerimiento
El motor de persistencia, debe de ser capas de recuperar un objeto indicando
únicamente el ID a recuperar.
El motor de persistencia debe de ser capas de construir el tipo de dato correcto sobre
el cual se cargaran los datos.
4.4.2 Diseño de la Solución
Dado el requerimiento anterior, se han diseñado dos métodos para realizar la carga
de un objeto a través de su ID. Se hará uso de la sobrecarga de funciones para
contar con métodos cargar que reciban ID de tipo numérico o string
Método
1
cargar
Parámetros
ID de tipo numérico
Descripción
Devuelve la instancia de la clase llena con
las propiedades identificados por el ID
numérico dado
2
cargar
ID de tipo string
Devuelve la instancia de la clase llena con
las propiedades identificados por el ID
string dado
Ya que este método solo recibirá un ID, para el desarrollador el tipo de dato a cargar,
dependerá de la clase DAO referenciada.
El método “cargar” hace uso de los servicios proporcionados por otros métodos de la
misma clase, esto se muestra en la siguiente secuencia:
Página 120 de 190
Maestría en Informática Aplicada a Redes
cargar
getObject
getWhereID
getDS
setPropiedades
RDBMS
Contruir Where con ID enviado
String para where
Enviar SQL y Solicitar Objeto
Envio de sentencia sql con filtro por ID
Exito o Exepcion
Solicitar Registro
Exito o Exepcion
Identificación de Tipo de Objeto
Creación de instancia de Tipo de Objeto
Envio de Registro y Domain Object
Domain Object Poblado
Instancia Domain Object Poblado
4.4.3 Construcción de la Solución
El código C# del método sobrecargado “cargar” tanto en su versión para id numérico
como string, se muestra a continuación:
// Metodo cargar para menejo de ID numericos
public object cargar(long id)
{
string sSQL;
object aDO;
try
{
sSQL = "select * from " + this._doName + getWhereID(id);
aDO = getObject(sSQL);
}
catch (Exception ex)
{
throw ex;
}
return aDO;
}
Página 121 de 190
Maestría en Informática Aplicada a Redes
// Metodo cargar para menejo de ID string
public object cargar(string id)
{
string sSQL;
object aDO;
try
{
sSQL = "select * from " + this._doName + getWhereID(id);
aDO = getObject(sSQL);
}
catch (Exception ex)
{
throw ex;
}
return aDO;
}
El método cargar una ves construido el SQL con filtro Where, delega la
responsabilidad de creación y poblado de los datos al método getObjet, el código de
este método, se muestra a continuación:
//Metodo privado que devuelve la instancia cargada del objeto a partir de un registro
private object getObject(string sSQL)
{
DataSet miDs;
object aDO;
try
{
string spaceName = this.GetType().AssemblyQualifiedName;
string objectDAOName = this.GetType().Namespace + "." +
this.GetType().Name;
string objectName = "Dominio." + this._doName ;
spaceName = spaceName.Replace(objectDAOName, objectName);
Type tTipo = Type.GetType(spaceName,true,true);
aDO = Activator.CreateInstance(tTipo);
miDs = this.getDS(sSQL);
if (miDs.Tables[0].Rows.Count > 0)
setPropiedades(miDs.Tables[0].Rows[0], aDO); else throw new
Exception("Problemas con el objeto " + this._doName );
}
catch (Exception ex)
{
Página 122 de 190
Maestría en Informática Aplicada a Redes
throw new Exception("Error en objeto " + this.GetType().Name + " / " +
ex.Message);
}
finally
{
miDs = null;
}
return aDO;
}
El método cargar utiliza en su secuencia de ejecución los métodos getWhereID que
devuelve un string con el filtro Where compuesto por el ID del objeto, por su lado el
método getobject utiliza el método setPropiedades el cual puebla las propiedades de
un objeto con los datos de un DataRow utilizando la Reflexión.
El código de ambos métodos, se detalla a continuación:
//Metodo que devuelve una cadena con la construccion del Where con los
primarykeys del objeto
private string getWhereID(string id){
DataColumn[] colArr;
string sColumn;
int ikeys;
try {
DataSet ds = getDsSchema();
colArr = ds.Tables[0].PrimaryKey;
ikeys = colArr.Length;
sColumn = " where " ;
if(ikeys==0) throw new Exception("Objeto " + _doName + " no tiene definido
Primary Key en el ER, imposible continuar");
for(int i = 1; i<ikeys;i++) sColumn += colArr[i].ColumnName + " = '" + id + "'";
} catch(Exception ex) {
throw new Exception("Error obteniendo Primary Keys / " + ex.Message );
}
return sColumn;
}
// Llenar el Domain Object a partir del DataRow invocando dinamicamente los Setter
de cada propiedad
private void setPropiedades(DataRow aRow, object aDO) {
PropertyInfo[] oProperties;
string sPropertyName="";
MethodInfo oMetodo;
Página 123 de 190
Maestría en Informática Aplicada a Redes
try {
oProperties =
aDO.GetType().GetProperties(BindingFlags.NonPublic|BindingFlags.Instance|Binding
Flags.Static|BindingFlags.Public);
foreach(PropertyInfo oProperty in oProperties) {
sPropertyName = oProperty.Name.ToString();
if(aRow.IsNull(sPropertyName)) {
oProperty.SetValue(aDO,null,null);
} else {
oMetodo = oProperty.GetGetMethod(true);
switch (oProperty.GetSetMethod(true).ReturnType.Name.ToString()) {
case "System.String":
oProperty.SetValue(aDO, (string)aRow[sPropertyName], null);
break;
case "System.DateTime":
oProperty.SetValue(aDO, (DateTime)aRow[sPropertyName], null);
break;
case "System.Int16":
oProperty.SetValue(aDO, (Int16)aRow[sPropertyName], null);
break;
case "System.Int32":
oProperty.SetValue(aDO, (Int32)aRow[sPropertyName], null);
break;
case "System.Int64":
oProperty.SetValue(aDO, (Int64)aRow[sPropertyName], null);
break;
case "System.Decimal":
oProperty.SetValue(aDO, (Decimal)aRow[sPropertyName], null);
break;
case "System.Double":
oProperty.SetValue(aDO, (Double)aRow[sPropertyName], null);
break;
case "System.Boolean":
Boolean bValor;
if(aRow[sPropertyName].GetType().ToString().CompareTo("System.SByte")==0) {
SByte sbValor = (SByte)aRow[sPropertyName];
bValor = (sbValor.ToString().Equals("1"))?true:false;
} else
if(aRow[sPropertyName].GetType().ToString().CompareTo("System.Byte")==0) {
Byte sbValor = (Byte)aRow[sPropertyName];
bValor = (sbValor.ToString().Equals("1"))?true:false;
} else {
bValor = (Boolean)aRow[sPropertyName];
}
oProperty.SetValue(aDO,bValor,null);
break;
Página 124 de 190
Maestría en Informática Aplicada a Redes
case "System.Char":
oProperty.SetValue(aDO, (string)aRow[sPropertyName], null);
break;
}
}
}
} catch(Exception ex) {
throw new Exception("Error seteando la propiedad " + sPropertyName + " / " +
ex.Message); }
}
4.5
Acceso Independiente del sistema RDBMS
4.5.1 Planteamiento del Requerimiento
La funcionalidad a construir ya que implementará la “Capa de Acceso a Datos”, debe
de ser capas de manejar eficientemente el acceso a distintos sistema de bases de
datos relacionales de forma transparente y parametrizada para el desarrollador. Esto
supone que el desarrollador solo deberá indicar dentro de la configuración del
aplicativo que motor de base de datos utilizará y el motor de persistencia debe de
hacer el resto. Sin embargo esto supone los siguientes problemas
Aunque ADO .Net 2.0 contiene una serie de objetos genéricos agrupados en el
namespace System.Data, entre otros el DataSet, DataTable, DataView, DataColumn,
DataRow, etc., al momento de interactuar con distintas bases de datos como SQL
Server u Oracle, hace uso de controles específicos para cada RDBMS. De esta
forma existe una clase SqlConenction para Microsoft SQL y una clase
OraConnection
para
Oracle,
así
como
MySqlConnections
para
Mysql
y
OleConnection para acceso via OleDB.
El uso de estas clases especificas por proveedor es necesaria ya que garantiza la
eficiencia en las operaciones en contra de cada contenedor.
Si bien esta considerado el uso de acceso vía OleDB a cualquier base con este tipo
de acceso, esto ocasiona una perdida de rendimiento por lo que es conveniente
Página 125 de 190
Maestría en Informática Aplicada a Redes
utilizar los conectores nativos ofrecidos por Microsoft o construidos por cada
proveedor.
La funcionalidad a construir requiere de la utilización de objetos específicos para
cada RDBMS tales como:
Clase
Descripción
Microsoft
Oracle
OleDB
SqlConnecti
OraConnecti
OleConnecti
on
on
on
SqlDataAda
OraDataAda
OleDataAda
pter
pter
pter
SqlComman
OraComman
OleComman
d
d
d
SqlDataRea
OraDataRea
OleDataRea
der
der
der
SQL
Connection
DataAdapter
Command
DataReader
Clase
Conexión
especifica
para
cada proveedor
Adaptador de datos
especifico
para
cada
proveedor,
permite incorporar
un contenido de
datos dentro de un
DataSet.
Clase Coomand, la
que ejecuta un sql
en una conexión
especifica
para
cada proveedor
Clase para acceso
conectado
especifica
para
cada
proveedor
(implementa
un
cursor
Forward
Only)
Página 126 de 190
Maestría en Informática Aplicada a Redes
4.5.2 Diseño de la Solución
Conociendo el requerimiento establecido. Donde se plantea la necesidad de crear
familias de objetos en tiempo de ejecución, el patrón idóneo para modelar una
solución de este tipo es el patrón Abstract Factory 6.
El framework .Net 2.0 posee dentro de ADO .Net 2.0 una implementación de este
patrón, el cual esta disponible en el namespace System.Data.Common, esta solución
permite la creación de objetos específicos para cada proveedor. Sin embargo a la
fecha de este estudio, únicamente existen Factorías concretas para SQL , Oracle y
OLE DB, las cuales son proporcionadas por Microsoft a través del mismo framework.
Proveedores como Mysql, Sybase, PostGress y el mismo Oracle ofrecen conectores
específicos para ADO .Net, pero estos no son heredados de la clase
System.Data.Comon por lo que no pueden ser utilizados para implementar el patrón
Abstract Factory. Dado esto para el caso de Proveedores distintos a SQL y Oracle
se utilizará el método de Acceso OLE DB, esperando que en pronto tiempo el resto
de proveedores liberen conectores que si puedan ser utilizados dentro de este
esquema.
Un esquema de la solución propuesta a través del patrón Abstract Factory
implementado en .Net, se muestra a continuación:
6
Ver numeral 3.4.7 para mayor información sobre el patrón Abstract Factory
Página 127 de 190
Maestría en Informática Aplicada a Redes
DAOMasterlibrary
<<Usa>>
<<Usa>>
<<Usa>>
DbConnection
{abstract}
DbProviderFactory
{abstract}
+
+
+
+
+
+
getFactory (string Provider)
createCommand ()
createConnection ()
createParameter ()
createDataAdapter ()
createTransaction ()
:
:
:
:
:
:
DbProviderFactory
DbCommand
DbConnection
DbParameter
DbDataAdapter
DbTransaction
SQLConnection
OraConnection
DbCommand
<<Crea>>
SQLProviderFactory
+
+
+
+
+
createCommand ()
createConnection ()
createParameter ()
createDataAdapter ()
createTransaction ()
:
:
:
:
:
SQLCommand
SQLConnection
SQLParameter
SQLDataAdapter
SQLTransaction
{abstract}
OraProviderFactory
+
+
+
+
+
createCommand ()
createConnection ()
createParameter ()
createDataAdapter ()
createTransaction ()
:
:
:
:
:
<<Crea>>
OraCommand
OraConnection
OraParameter
OraDataAdapter
OraTransaction
OraCommand
SqlCommand
<<Crea>>
<<Crea>>
ADO .Net provee la clase Abstracta DbProviderFactory la cual es capas de crear
instancias de clases xxConnections, xxCommand, xxDataAdapter, etc. Estos son
referenciados a través de productos abstractos DbConnection, DbCommand,
DbDataAdapter que en realidad contienen objetos concretos de tipo SqlConnection,
SqlCommand, SqlDataAdapter u OraConnection, OraCommand, OraDataAdapter
para el caso de Oracle.
Dado que dentro de la funcionalidad a crear se deberá manejar mecanismos de crear
la DBFactory adecuada, así como crear las cadenas de conexión correctas, etc. Se
deberá diseñar una clase separada para implementar estos mecanismos la cual
proporcionara sus servicios a la clase principal que implementara el patrón DAO.
Dado que la clase DAOMaster implementara los métodos de persistencia, la
responsabilidad de crear el DBFactory correcta, así como el de crear conexiones a la
base de datos caerá sobre una nueva clase denominada DataFactory cuyos servicios
Página 128 de 190
Maestría en Informática Aplicada a Redes
serán usados por la clase principal DAOMaster. Esta relación se muestra en el
siguiente diagrama.
DAOMaster
0..1
<<Usa>>
dataFactory
DAOMasterLibrary
DataFactory
- dataFactory : DataFactory
- dbFactory
: DbProviderFactory
- dbCon
: DbConnection
+
+
+
-
DbFactory ()
:
getInstancia ()
:
getFactory ()
:
getConnection () :
setDB ()
:
1..1
= new DataFactory()
void
DataFactory
DbProviderFactory
DbConnection
void
DBProviderFactory
<<Crea>>
4.5.3 Construcción de la Solución
El procedimiento de utilización de la Factory consiste en los siguientes pasos:
1. Definición de la Factory
Para realizar esta tarea bastara con definir una instancia de tipo DbProvider
Factory de la siguiente manera:
DbProviderFactory DbFactoria;
2. Instanciamiento de la Factory de acuerdo al Proveedor
Teniendo la definición del tipo de dato, pasaremos al proceso de creación de
la instancia a través del método getFactory() el cual devuelve una instancia
Página 129 de 190
Maestría en Informática Aplicada a Redes
concreta de acuerdo al argumento enviado el cual debe de indicar el
namespace del proveedor a utilizar, así:
DbFactoria = DbProviderFactory.GetFactory(“System.Data.SqlClient”);
Crea una instancia de una factory para SQL Server.
3. Definición de productos abstractos
Para utilizar objetos de tipo Connection, Command , TableAdapter o
DataAdapter dentro del código del motor de persistencia se hace referencia a
objetos de tipo DB como se muestra en las siguientes definiciones
DbConnection conn;
DbCommand command;
DbTableAdapter tableAdapter;
DbDataAdapter dataAdapter;
4.
Creación de Productos concretos.
Para la instanciación de objetos concretos se hará referencia a la clase DbFactoria
como se muestra a continuación.
Conn = DbFactoria.createConnection() ;
Command = DbFactoria.createCommand();
tableAdapter = DbFactoria.createTableAdapter();
dataAdapter = DbFactoria.createDataAdapter();
El código C# de la clase DataFactory se muestra a continuación:
using System;
using System.Data.Common;
namespace DAOMasterLibrary
{
class DataFactory
{
static DataFactory dataFactory = new DataFactory();
Página 130 de 190
Maestría en Informática Aplicada a Redes
DbProviderFactory dbFactory;
DbConnection dbCon;
private void DbFactory()
{
}
static public DataFactory getInstancia()
{
return dataFactory;
}
public DbProviderFactory getFactory()
{
if (dbFactory == null) setDB();
return dbFactory;
}
public DbConnection getConnection()
{
try
{
if (dbCon == null)
{
setDB();
dbCon = dbFactory.CreateConnection();
dbCon.ConnectionString = "";
dbCon.Open();
}
}
catch (Exception ex)
{
throw new Exception("Imposible Crear Conexión a RDBMS / " +
ex.Message);
}
return dbCon;
}
private void setDB()
{
dbFactory = DbProviderFactories.GetFactory("System.Data.SqlClient");
}
}
}
Página 131 de 190
Maestría en Informática Aplicada a Redes
4.6
Configuración y Parametrización
4.6.1 Planteamiento del Requerimiento
Para que el motor de persistencia sea reutilizable en cualquier aplicación, debe de
permitir su parametrización, es decir el desarrollador al utilizar esta funcionalidad
debe poder indicar el tipo de dialecto o contenedor a utilizar, indicar parámetros de
servidor, usuario, password y bases de datos, etc.
4.6.2 Diseño de la Solución
Para definir los parámetros como datos de la conexión, dialectos, etc. Se ha
considerado la creación de un archivo XML el cual será denominado DAOConfig.ini,
este contendrá entre otros los siguientes parámetros.
•
Dialecto: Es el nombre del sistema RDBMS a utiliza, podrá tomar los valores
SqlClient, Oracle, MySql, OleDB.
•
DataSource: Es el nombre del servidor o la ip address del datasource.
•
Usuario: El nombre del usuario con el cual se crearán las conexiones hacia el
datasource
•
Password: El password del usuario a conectarse.
•
DataBase: El nombre de la base de datos a la cual se deberá conectar la
aplicación.
4.6.3 Construcción de la Solución
El archivo DAOConfig deberá ser ubicado en el mismo directorio donde se ubique la
librería DAOMasterLibrary.dll, como generalmente se agregará como referencia a un
proyecto .Net. El directorio por defecto será el directorio bin el cual ya esta protegido
para no descargar información desde dicha ruta.
Página 132 de 190
Maestría en Informática Aplicada a Redes
Un ejemplo del archivo de configuración, se muestra a continuación:
<DAOConfig>
<Parametros>
<Dialecto>SqlClient</dialecto>
<DataSource>Localhost<DataSource>
<Usuario>sa<Usuario>
<Password>daoMasterSA<Password>
<DataBase>Northwind<DataBase>
</Parametros>
</DAOConfig>
Dado que la responsabilidad de crear y manejar las conexiones es de la clase
DataFactory, esta que es implementada a través del patrón Singleton, tendrá una
serie de propiedades asociadas a los parámetros ingresados en el archivo XML
namespace DAOMasterLibrary
{
class DataFactory
{
private string _dialecto;
private string _datasource;
private string _database;
private string _usuario;
private string _password;
…
De igual forma esta clase posee el método loadParameters() el cual se encarga de
leer el archivo XML cargando los parámetros a las propiedades de la clase. La
implementación de este método, se muestra a continuación:
private void loadParameters()
{
XPathDocument doc;
XPathNavigator nav;
Página 133 de 190
Maestría en Informática Aplicada a Redes
try
{
StreamReader stFile = new StreamReader("DAOConfig.xml");
string xml = stFile.ReadToEnd();
stFile.Close();
doc = new XPathDocument(new StringReader(xml));
nav = doc.CreateNavigator();
_dialecto = nav.SelectSingleNode("/*/Parametros/Dialecto").InnerXml;
_datasource = nav.SelectSingleNode("/*/Parametros/DataSource").InnerXml;
_database = nav.SelectSingleNode("/*/Parametros/DataBase").InnerXml;
_usuario = nav.SelectSingleNode("/*/Parametros/Usuario").InnerXml;
_password = nav.SelectSingleNode("/*/Parametros/Password").InnerXml;
}catch (Exception ex) {
throw new Exception("Problemas cargando parametros de conexion al
datasource / " + ex.Message );
}
}
Página 134 de 190
Descargar