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