Índice de la sección dedicada a .NET (en el Guille) ADO .NET

ENCAPSULACIÓN DE ADO.NET

Autor: Erik
Fecha: 23/Feb/2003

 

 

 

¿ADO.NET ES UN PASO ADELANTE?

 

Fue esta pregunta la que me movió a escribir este artículo. No hace mucho tuve una charla con alguien que no estaba muy convencido de que ADO.NET le fuera a suponer una mejora real a la hora de trabajar. Lo cierto es que no me costó mucho convencerle de lo contrario pero, debido a que no tuve oportunidad de hacerle una demostración práctica de todo lo que le estaba contando, me quedó la extraña (e incómoda) sensación de que solamente me había creído a medias.

 

He de reconocer que el hecho de que nuestros queridos Recordsets de toda la vida hubieran sido eliminados dejaba un cierto poso de desconfianza al principio. De hecho, a mí me fue inevitable pensar: "Ya estamos. Le habrán cambiado el nombre, como de costumbre... Pero fijo que sigue siendo lo mismo". Craso error el mío en aquellos primeros momentos porque, efectivamente, ADO.NET cambia radicalmente el planteamiento del modelo de objetos con el propósito de mejorar sustancialmente el rendimiento de aplicaciones cliente-servidor de varios niveles, haciéndolo, además, mucho más fácil de encapsular. No puede haber, por lo tanto, objetos Recordset como los que teníamos en las versiones anteriores porque ya no encajan en la nueva estructura. Así, ADO.NET proporciona un acceso a datos mucho más ágil y eficiente a todos los niveles, como veremos más adelante.

 

En este artículo mostraré cómo encapsular ADO.NET en una biblioteca de clases. Empezaremos con un poco de teoría, siempre imprescindible en estos tiempos que corren, para luego entrar de lleno en lo que verdaderamente nos gusta (por lo menos a mí), que es la implementación en C#.

 

Al final, estoy convencido de que alcanzarás a ver las mejoras de la nueva filosofía que incorpora ADO.NET, sobre todo si habías utilizado ADO anteriormente, y llegarás a la misma conclusión que yo: ADO.NET sí es un paso adelante. Ahora bien, si quieres adaptarte realmente al nuevo modelo de objetos, lo mejor que puedes hacer no tratar de buscar equivalencias con el modelo anterior, porque, sencillamente, casi (y digo casi) se puede decir que no existen.

 

¿PARA QUÉ ENCAPSULAR ADO?

 

Como estoy seguro de que más de uno se hará esta pregunta, voy a anticiparme a ella y dar algunas de las poderosas razones que lo aconsejan.

 

Si en todos los años que llevo trabajando en esto estoy seguro de haber aprendido algo es esto: los controles enlazados, generalmente, nos hacen perder demasiado control sobre la aplicación, lo cual hace que su uso no sea aconsejable salvo raras excepciones, motivo por el cual tenemos que escribir bastante código para conseguir la funcionalidad que deseamos. El problema radica en que el código de acceso a datos es siempre engorroso, fastidioso, insidioso, tedioso y trabajoso, a pesar de que los modelos de objetos son cada vez más sencillos.

 

¿Por qué es engorroso? Pongamos, por ejemplo, que tenemos que diseñar una aplicación para que un hotel pueda almacenar y gestionar información de habitaciones, restaurante, tienda, lavandería, aparcamiento, contabilidad, nóminas, etc. Ante esto tenemos varias opciones:

 

Podemos desarrollar varias aplicaciones ligeras, cada una para la gestión de una parte, pero todas ellas operando contra el mismo origen de datos, obviamente, dado que en muchos casos la información tendrá que ser compartida. Así, por ejemplo, el que esté en el aparcamiento tendrá una aplicación para gestionar lo que le corresponde, pero no podrá reservar habitaciones, por ejemplo, porque de esto se ocupará el de recepción con otra aplicación específica para ello. Sin embargo, el del aparcamiento no deberá estar completamente aislado de la recepción, dado que el aparcamiento es exclusivamente para clientes, de modo que necesita saber si una persona que quiere aparcar está registrada en el hotel. Por otro lado, el de contabilidad tendrá que tener en cuenta los datos de cada uno de los departamentos que generen gastos o ingresos, aunque no podrá generar facturas del restaurante, o la lavandería. Este sistema tendría la ventaja de que cada aplicación sería bastante ligera y fácil de mantener, puesto que la modificación de una no implicaría en modo alguno tener que tocar las demás, pero tiene la pega importante de que habría que repetir muchos fragmentos de código que serían comunes en varias de ellas.

 

Otra opción (en mi opinión, menos adecuada) sería desarrollar una mega-aplicación, que soportara la funcionalidad de todos los departamentos, y asignar los derechos de los usuarios según sus funciones específicas. Sin embargo, aquí tenemos la pega de que es mucho más pesada y difícil de mantener, puesto que para modificar una parte habría que volver a generar e instalar todo el proyecto en cada departamento.

 

Sin embargo, hay un problema que será común para cualquiera de las dos opciones que vayamos a elegir, y es que, en ambos casos, habrá que implementar una cantidad de código de acceso a datos importante, con el problema añadido de que este se repetirá muchas veces en partes distintas si no tenemos un poco de precaución. No obstante, el mayor problema no es este, sino el hecho de que tanto la funcionalidad de la aplicación como su implementación están juntas y serán muy difíciles de separar, e imposible de pasar a otro lenguaje. Además, si, después de un tiempo, se estudia hacer alguna modificación en la estructura del origen de datos, por trivial que sea dicha modificación, nos daremos cuenta inmediatamente de que no es posible sin tener que efectuar importantes modificaciones también en cada una de las aplicaciones que trabajan contra él.

 

En consecuencia, la encapsulación del código de acceso a datos en una biblioteca de clases aparece como la solución más versátil y viable para este trabajo. El efecto inmediato es sumamente positivo, pues conseguiremos separar completamente la funcionalidad de cada aplicación con su implementación específica. Lo que se consigue, por un lado, es escribir el código engorroso una única vez, y hacer que este sea completamente reutilizable para cualquier otra aplicación (y para cualquier lenguaje). Por otro lado, las modificaciones sobre el origen de datos provocarán modificaciones también en dicha biblioteca de clases, pero no tienen por qué afectar al resto de las aplicaciones que trabajan con ella.

 

LOS ESPACIOS DE NOMBRES DE ADO.NET

 

ADO.NET se encuentra en la biblioteca System.Data.dll, y ofrece clases en cinco espacios de nombres bien diferenciados que explico brevemente a continuación:

 

System.Data: es el espacio de nombres primario. Dentro de este espacio de nombres tenemos un conjunto de clases que representan, digamos, una base de datos virtual, tablas, filas, columnas, relaciones, etc. Sin embargo, ninguna de estas clases ofrece conexión alguna con un origen de datos, sino que simplemente representan los datos en sí mismos.

 

System.Data.Common: ofrece clases comunes entre distintos orígenes de datos. Para lo que vamos a tratar en este artículo, podemos decir que estas clases sirven de clase base para las que están contenidas en los dos espacios de nombres que vienen a continuación.

 

System.Data.OleDb: contiene una serie de clases que nos permiten conectarnos con cualquier origen de datos e interactuar con él al tiempo que sirven de "intermediarios" entre el origen de datos y las clases del espacio de nombres System.Data que, según decíamos, no tienen conexión alguna con dicho origen de datos. Las clases de System.Data.OleDb usan OLEDB como tecnología subyacente.

 

System.Data.SqlClient: contiene clases que permiten interactuar con orígenes de datos SQL Server de un modo mucho más directo que OLEDB, mejorando el rendimiento para este tipo de origen de datos. Por lo tanto, solamente se pueden utilizar para acceder a bases de datos de SQL Server. El uso de sus clases es prácticamente equivalente al de las que se encuentran en System.Data.OleDb.

 

System.Data.SqlTypes: este espacio de nombres ofrece los tipos primitivos que usa SQL Server. Obviamente, aunque se pueden usar los tipos equivalentes del CTS, los que se incluyen en este espacio de nombres están optimizados para trabajar con SQL Server.

 

A pesar de que el modelo de objetos ha sido simplificado, aún sigue siendo lo suficientemente extenso como para no poder entrar en detalle con todas sus clases en sólo un artículo, sobre todo si no quiere uno agobiar a los sufridos lectores más de lo estrictamente necesario. Por este motivo, nos centraremos más en algunas clases fundamentales de los espacios de nombres System.Data y System.Data.SqlClient, desarrollando para ello un ejemplo completo. Daremos también alguna pequeña pincelada con clases OleDb, pero no reproduciremos un ejemplo completo en este caso porque sería casi idéntico al desarrollado con SqlClient.

 

LAS CLASES BÁSICAS DE ADO.NET

 

Las he llamado básicas porque, realmente, serán el pilar fundamental para casi cualquier aplicación que tenga que acceder a un origen de datos con unos mínimos requisitos de eficiencia y escalabilidad. Las clases más importantes del espacio de nombres System.Data son:

 

DataSet: aun a riesgo de equivocarme, diré que es la piedra angular del modelo de objetos: Esta clase permite tener en memoria una auténtica "base de datos virtual", con sus tablas, relaciones, etc. Un hecho importante es que esta base de datos virtual está total y absolutamente desconectada de cualquier origen de datos físico y, en consecuencia, siempre se aloja toda entera en la memoria. En otras palabras, puede contener uno o varios conjuntos de filas distintos, que pueden estar o no estar relacionados entre sí, pero siempre en la memoria y siempre desconectados del origen de datos. La clase DataSet está dentro del espacio de nombres System.Data.

 

DataTable: un DataTable representa un conjunto de filas y columnas también en memoria y desconectado del origen de datos, como el DataSet. Pertenece al espacio de nombres System.Data. La propiedad Tables de un objeto DataSet contiene una colección de objetos DataTable, y dicha colección es de la clase DataTableCollection. Por otra parte, cada objeto DataTable representa sus filas en la propiedad Rows (de la clase DataRowCollection), siendo cada fila, a su vez, un objeto de la clase DataRow, y sus columnas en la propiedad Columns (de la clase DataColumnCollection), siendo cada columna un objeto de la clase DataColumn.

 

DataRelation: representa una relación entre dos objetos DataTable. También pertenece al espacio de nombres System.Data. Todas las relaciones que haya en un DataSet se encuentran en la colección Relations, de la clase DataRelationCollection.

 

Como podrás suponer, hay más, pero no serán necesarias para el desarrollo de este artículo. Ahora, vamos a pasar a ver las clases más importantes del espacio de nombres System.Data.SqlClient, y sus equivalentes en el espacio de nombres System.Data.OleDb:

 

SqlConnection: su equivalente en OleDb es OleDbConnection. Son más o menos equivalentes a la clase Connection del antiguo ADO, en tanto en cuanto que proporcionan la conexión con el origen de datos y mantiene algunas de sus antiguas propiedades y métodos, como son ConnectionString, ConnectionTimeOut, Open y Close. Sin embargo, al igual que las otras clases que también tienen un equivalente (más o menos) en la tecnología antigua, ten presente que no se manejan exactamente igual, como tendremos ocasión de ver más adelante.

 

SqlCommand: su equivalente en OleDb es OleDbCommand. También son parecidos a los antiguos objetos Command de ADO y, como estos, representan procedimientos almacenados o instrucciones SQL que se ejecutan en el origen de datos.

 

SqlParameter: su equivalente en OleDb es OleDbParameter. Del mismo modo que los dos anteriores, se parecen a los objetos Paremeter del antiguo ADO, y representan un parámetro dentro de la colección Parameters del objeto SqlCommand u OleDbCommand, según el caso. La colección Parameters de un objeto SqlCommand es de la clase SqlParameterCollection (OleDbParameterCollection en OleDb).

 

SqlDataAdapter: su equivalente en OleDb es OleDbDataAdapter. Esta clase no tiene nada parecido (ni de lejos) en el antiguo ADO. Contiene un conjunto de objetos SqlCommand (ú OleDbCommand, según proceda) en sus propiedades SelectCommand, InsertCommand, UpdateCommand y DeleteCommand. Cuando se invoca el método Fill, el DataAdapter rellena un objeto DataSet o DataTable con el conjunto de filas resultante de ejecutar el comando establecido en su propiedad SelectCommand. Cuando se invoca el método Update, el DataAdapter ejecuta el comando establecido en su propiedad InsertCommand para añadir al origen de datos las filas nuevas añadidas a un DataTable, el comando UpdateCommand para modificar las filas que hayan sido modificadas en el DataTable y el comando DeleteCommand para eliminar las filas que hayan sido eliminadas en el DataTable. Por lo tanto, el DataAdapter es el nexo que une los objetos DataSet y DataTable, totalmente desconectados, con el origen de datos físico. Si la conexión estaba cerrada antes de ejecutar los métodos Fill o Update, el DataAdapter se ocupa de abrir dicha conexión para efectuar la operación requerida, cerrando de nuevo la conexión una vez que ha terminado. Si la conexión estaba abierta, el DataAdapter deja que dicha conexión siga abierta después de haber terminado.

 

SqlCommandBuilder: su equivalente en OleDb es OleDbCommandBuilder. Tampoco había nada en el antiguo ADO que hiciera lo que hace esta clase. Sencillamente se ocupa de generar los objetos command necesarios para un determinado DataAdapter, gracias a los métodos GetInsertCommand, GetUpdateCommand y GetDeleteCommand.

 

Aquí también tenemos más clases, pero tampoco van a hacernos falta para nuestro ejemplo, de modo que las pasaremos por alto.

 

Resumiendo un poco todo esto, ADO.NET ofrece un modo de trabajo completamente desconectado por medio del DataSet, el DataTable y el DataAdapter. En primer lugar se cargan los datos en un DataSet o en un DataTable, el cual se aloja en la memoria del cliente. Éste efectúa los cambios necesarios en el conjunto de datos alojado en la memoria sin necesidad de mantener una conexión persistente con el servidor (es decir, el origen de datos), con lo cual el rendimiento aumenta considerablemente. Después, si se desea almacenar los cambios, se ejecuta el método Update del DataAdapter para que este haga los cambios pertinentes en el origen de datos, manteniéndose desconectado el DataSet.

 

Bien es cierto que en ADO también se podía trabajar de un modo similar usando Recordsets desconectados. Sin embargo esta técnica estaba bastante más limitada. Para guardar los cambios efectuados en un Recordset desconectado había que utilizar como intermediario otro Recordset, ese último conectado. Además, el método UpdateBatch efectuaba las modificaciones pertinentes en el origen de datos, pero no ofrecía tantas posibilidades de personalizar el modo de hacerlo como ofrece el DataAdapter de ADO.NET al proporcionarnos la posibilidad de especificar objetos Command propios, para las operaciones de inserción, actualización y eliminación de filas. Además de esto, como los distintos Recordsets del antiguo ADO no podían relacionarse entre sí no merecía la pena mantener abierto un Recordset con una sola fila, aunque estuviera desconectado, lo cual hacía que fuera más difícil de encapsular.

 

Seguro que a estas alturas estarás pensando en que trabajar completamente desconectado del origen de datos puede provocar problemas de concurrencia. Ciertamente, así es, y veremos cómo solucionarlos, pero creo que es mejor hacerlo más adelante, cuando hayamos avanzado un poco en la parte práctica.

 

EL EJEMPLO COMPLETO

 

LA BASE DE DATOS

 

Trabajaremos con una pequeña base de datos relacional. Pequeña para no extenderme demasiado en cuestiones sumamente repetitivas, pero lo suficientemente elaborada como para hacer extensible este ejemplo a casi cualquier situación de la vida real.

 

La base de datos que vamos a emplear se ocupará de almacenar clientes y facturas. Lógicamente, la información se debe separar en tres tablas convenientemente relacionadas: una para clientes, otra para facturas y otra para los detalles de cada factura. Por lo tanto, el esquema de nuestra base de datos es el siguiente:

 

 

No me voy a perder en disquisiciones sobre cómo se diseña la base de datos con SQL Server, Access o lo que queráis utilizar, porque si estás leyendo esto es de suponer que ya sabes hacerlo. Sí daré, no obstante, una descripción más detallada de cada campo y relación, no vaya a ser que alguien se me pierda por haber omitido algo que cuesta tan poco incluir.

 

TABLA COLUMNA TIPO SQLSvr TIPO Access Clave Principal
Facturas idFactura int identity Autonumérico

idCliente int Numérico (Long) No
Fecha datetime Fecha/Hora No
Numero char(12) Texto (12) No
IVA smallint Byte No
         
Clientes idCliente int identity Autonumérico
Nombre char(15) Texto (15) No
Ape1 char(15) Texto (15) No
Ape2 char(15) Texto (15) No
         
Detalles idDetalle int identity Autonumérico
Cantidad int Numérico (Long) No
Descripcion char(100) Texto (100) No
Precio money Moneda No
idFactura int Numérico (Long) No

 

La tabla Clientes se relaciona uno a varios con la tabla Facturas, esto es, por cada cliente puede haber cero, una o varias facturas. La relación se establece entre los campos "idCliente" de cada tabla. Por su parte, la tabla Facturas se relaciona también uno a varios con la tabla Detalles, por medio de los campos "idFactura" de cada una.

 

En el caso de la base de datos SQL Server se deben incluir también, al menos, un par de procedimientos almacenados (aunque lo suyo sería escribir 3), pero estos se explicarán más adelante.

 

¿POR DÓNDE EMPEZAMOS?

 

Antes de seguir adelante, quisiera aclarar que vamos a desarrollar el ejemplo del modo más sencillo posible. Ciertamente, a lo que vamos a hacer aquí se le podrían añadir bastantes más cosas, como implementar la interface IComponent y/o añadir más métodos (para buscar, para eliminar, para ofrecer esquemas XML de la información, etc). Sin embargo, repito, lo que pretendo es solamente mostrar cómo encapsular ADO.NET, de modo que nos ahorraremos todo eso con el fin de conseguir una mejor comprensión del tema que estamos tratando.

 

Nos estábamos preguntado que por dónde íbamos a empezar. Pues bien, como se suele hacer en estos casos, empezaremos por el principio, es decir, preguntarnos qué queremos hacer. En general, es más fácil saber cómo hacer algo cuando sabemos qué es lo que queremos hacer, ;-)

 

Ante esta pregunta estoy seguro que todos sabéis ya la respuesta: como este artículo trata sobre la encapsulación de ADO.NET, habrá que hacer una biblioteca de clases que encapsule el código de acceso a este origen de datos. Después se podrán diseñar cientos de aplicaciones distintas (de consola, de Windows, aplicaciones Web, servicios...) que operen a través de esta biblioteca de clases sin necesidad de utilizar ni uno sólo de los objetos de ADO.NET. Ahora toca preguntarse cómo lo hacemos.

 

Generalmente, este es uno de los dilemas más importantes a la hora de encapsular en una biblioteca de clases el acceso a un origen de datos, y de las decisiones que se tomen en este punto dependerá después que todo el proyecto funcione bien, regular, mal, muy mal, horriblemente mal o tan mal que sea imposible hacerlo peor. Una de las peores soluciones sería, por ejemplo, optar por una colección que cargara absolutamente todo el contenido de la base de datos en un DataSet, y manejarnos a partir de ahí. Si lo pensamos un poco, esta no es la mejor opción de que disponemos. ¿Qué ocurre si la base de datos tiene, por ejemplo, 3000 facturas almacenadas (y no son muchas)? Pues que habría que cargar 3000 facturas para que el usuario final pudiera modificar una. Esto se pone mucho peor si pensamos que, por ejemplo, cada factura tiene 3 líneas de detalle: habría que cargar 3000 registros de la tabla de Facturas más otros 9000 de la tabla de detalles para que el usuario pudiera modificar una ridícula, minúscula y escasísima factura, lo cual es una barbaridad, qué barbaridad, por los clavos de Cristo, qué barbaridad...

 

Normalmente es el propio origen de datos el que nos dice cómo tenemos que diseñar la biblioteca de clases. En este caso, la base de datos pretende almacenar facturas, ¿no? Pues bien, estableceremos que la clase principal será la clase Factura, la cual habrá de proporcionar propiedades para manejar los datos de la misma, el cliente y la colección de detalles. De este modo, la aplicación final se basará en la creación y edición de facturas, pues seguramente es lo que se pretende. Esto no quiere decir que haya que reducir la flexibilidad de nuestra biblioteca de clases. En este caso, es más que probable que alguna de las aplicaciones finales también necesite editar clientes, y esta es una posibilidad que también debemos cubrir. Por lo tanto, habrá que escribir también una clase cliente que proporcione las propiedades necesarias para poder manejar los datos de un cliente, independientemente de las facturas.

 

Pero aún nos queda algo: Cuando un usuario quiera abrir un cliente o una factura para modificarla, necesariamente tendrá que ver los clientes o las facturas que hay ya almacenadas. ¿Cómo podemos hacerlo? Pues aquí la decisión hay que basarla en el tamaño que se presume que tendrá la base de datos, pensando sobre todo en que esta decisión condicionará enormemente su rendimiento. Si se presume que la base de datos será pequeña, podemos ofrecer una colección que proporcione una lista completa de clientes y otra que proporcione una de facturas, pero ojo, sólo una lista de lo que hay, quizá con los datos más relevantes, ya que es probable que no sean necesarios todos para hacer una simple búsqueda. Si se presume que la base de datos puede alcanzar un tamaño importante, quizá esta opción deje de ser eficaz, puesto que necesitaría demasiada memoria. Ante esto podemos ofrecer alguna colección en la que la aplicación cliente tenga que introducir, obligatoriamente, algunos criterios de búsqueda, por ejemplo facturas entre una fecha y otra, o facturas emitidas a un determinado cliente, o clientes que se apelliden tal o cual. Si, a pesar de esto, pensamos que estas colecciones seguirían siendo demasiado pesadas, podemos optar también por escribir un delegado que vaya pasando la información al cliente objeto a objeto. De este modo cada objeto será destruido y, en consecuencia, será recolectable por el GC una vez que haya sido procesado por la aplicación cliente en cada ejecución del delegado.

 

En nuestro ejemplo vamos a optar por la primera opción, esto es, diseñaremos una colección Facturas y una colección Clientes. Emplearemos un indizador e implementaremos las interfaces IEnumerable e IEnumerator, haciendo posible su iteración en un bucle foreach. Ahora bien, creo que sería conveniente que los objetos de estas colecciones fueran de sólo lectura ¿Por qué? Pues porque, por ejemplo, el listado de facturas debería ofrecer los datos de la factura y el cliente en cada uno de sus elementos para poder identificarla, pero no la lista de detalles de cada una. Además, así obligamos a que estas colecciones se usen solamente para lo que están, esto es, buscar algo en un momento dado, y no para estar en la memoria constantemente dado que, previsiblemente, podrían llegar a ser demasiado pesadas.

 

Ahora que lo pienso, si voy a definir unas clases para una Factura y un Cliente, digamos, editables, y otras para las facturas y clientes de sólo lectura que estarán en sus respectivas colecciones ListaFacturas y ListaClientes, creo que el diseño de un par de interfaces, IFactura e ICliente, con los miembros comunes de cada clase, puede mejorar notablemente el resultado final.

 

Así pues, después de todas estas elucubraciones, creo que ya tenemos ideada una buena estructura para nuestra biblioteca de clases. Si no estás habituado a encapsular código en bibliotecas de clases es probable que ahora mismo tengas un cierto desbarajuste mental con todo esto. Sin embargo, creo que cuando veas el código y, sobre todo, cuando lo veas funcionar, te harás una idea mucho más clara.

 

IMPLEMENTACIÓN DE LA BIBLIOTECA DE CLASES

 

Como todas las clases que vamos a implementar necesitarán un objeto SqlConnection, lo mejor será definirlo como un campo static en una clase que se ocupe de crear dicho objeto en un constructor static. La llamaremos dbSrc. Así evitaremos tener que crear un objeto SqlConnection para cada una de las instancias que se puedan crear de las demás clases. También podríamos crear este campo static en la clase Factura, o en la clase Cliente, por ejemplo, pero prefiero hacerlo en una clase aparte por una cuestión de orden principalmente, ya que la conexión va a ser común para todas las clases, y no sólo para la clase Factura o Cliente en concreto. Esta clase deberá proporcionar también un par de propiedades static, para que, desde la aplicación cliente, se puedan pasar el usuario y su contraseña. Ahora bien, el objeto SqlConnection deberá ser internal, puesto que al cliente le importarán un rábano los detalles de este objeto. Para eso estamos encapsulando ADO.NET.

 

Por otra parte, encerraremos todas nuestras clases dentro de un espacio de nombres adecuado, con el objeto de evitar posibles conflictos de nombres con otras bibliotecas. Así, por ejemplo, yo llamaré a dicho espacio de nombres Erik.FacturasLib.

 

using System;

using System.Data;

using System.Data.SqlClient;

using System.Collections;

 

namespace Erik

{

    namespace FacturasLib

    {

        public class dbSrc

        {

            internal static SqlConnection cnn;

            static string usuario;

            static string contraseña;

 

            static dbSrc()

            {

                cnn=new SqlConnection();

            }

 

            internal static void AsignarCadenaConexion()

            {

                cnn.ConnectionString="server=OEMCOMPUTER\\VSdotNET;database=facturas;"+

                    "user id="+Usuario+";password="+Contraseña;

            }

 

            public static string Usuario

            {

                get

                {

                    return usuario;

                }

                set

                {

                    if (value.IndexOf(";")>=0)

                        throw new Exception("Nombre de usuario no válido");

                    else

                    {

                        usuario=value;

                        AsignarCadenaConexion();

                    }

                }

            }

 

            public static string Contraseña

            {

                get

                {

                    return contraseña;

                }

                set

                {

                    if (value.IndexOf(";")>=0)

                        throw new Exception("Contraseña no válida");

                    else

                    {

                        contraseña=value;

                        AsignarCadenaConexion();

                    }

                }

            }

 

            public static bool Verificar()

            {

                try

                {

                    cnn.Open();

                }

                catch

                {

                    return false;

                }

 

                cnn.Close();

                return true;

            }

        }

        ...

 

Hay algunas cosas importantes que comentar antes de seguir. En primer lugar, quiero que os fijéis en que la conexión solo se abre (con la llamada al método Open) en el método static Verificar, y después se cierra inmediatamente. ¿Por qué? Recordemos que el DataAdapter ya se ocupará de abrir y cerrar la conexión automáticamente cada vez que lo necesite, de modo que podemos tener la conexión siempre cerrada sin ningún tipo de pudor. Por otra parte, en las propiedades Usuario y Contraseña habría que lanzar una excepción si se pretendiera incluir alguna de las palabras clave de la cadena de conexión. Por ejemplo, alguien podría poner como usuario esto:

 

NombreUsuario;Trusted_Connection=yes

 

Lógicamente, esto es algo que no deberíamos permitir bajo ningún concepto, y lo mismo para la propiedad Contraseña. Por este motivo hemos usado propiedades, en lugar de usar los campos directamente. Controlar esto es fácil, pues basta con rechazar cualquier cadena que contenga algún punto y coma.

 

A continuación vamos a definir las interfaces IFactura e ICliente, las cuales nos van a resultar de mucha utilidad, como veremos más adelante:

 

public interface IFactura

{

    string Numero { get; set; }

 

    DateTime Fecha { get; set; }

 

    byte IVA { get; set; }

 

    int idFactura { get; }

}

 

public interface ICliente

{

    string Nombre { get; set; }

 

    string Ape1 { get; set; }

 

    string Ape2 { get; set; }

 

    int idCliente { get; }

}

 

Como se puede observar, las interfaces solamente exigirán que se implementen las propiedades que coinciden con los campos en la base de datos. La razón es que cualquier objeto que implemente, por ejemplo, ICliente, deberá proporcionar como mínimo esas propiedades. Luego se podrán añadir más cosas, pero como mínimo debe cumplir estos requisitos. Por otra parte, las propiedades que deben retornar el valor del campo de clave principal han de ser de sólo lectura, pues de los datos de dicho campo se ocupa únicamente el origen de datos.

 

Continuaremos ahora con la clase que iba a proporcionar una lista de facturas (ListaFacturas). Dado que será una colección (aunque de sólo lectura), deberemos implementar también una clase para cada uno de los elementos que habrá en el listado. Pues bien, a esta clase la llamaremos FacturaEnListado, y es esta:

 

public class FacturaEnListado:IFactura,ICliente

{

    private string numero="";

    private DateTime fecha=DateTime.Today;

    private byte iva=0;

    private int idfactura=0;

    private string nombre="";

    private string ape1="";

    private string ape2="";

    private int idcliente=0;

 

    internal FacturaEnListado(string numero, DateTime fecha,

        byte iva, int idfactura, string nombre, string ape1,

        string ape2, int idcliente)

    {

        this.numero=numero.Trim();

        this.fecha=fecha;

        this.iva=iva;

        this.idfactura=idfactura;

        this.nombre=nombre.Trim();

        this.ape1=ape1.Trim();

        this.ape2=ape2.Trim();

        this.idcliente=idcliente;

    }

 

    public string Numero

    {

        get

        {

            return this.numero;

        }

        set

        {

            throw new Exception("La propiedad es de sólo lectura");

        }

    }

 

    public DateTime Fecha

    {

        get

        {

            return this.fecha;

        }

        set

        {

            throw new Exception("La propiedad es de sólo lectura");

        }

    }

 

    public byte IVA

    {

        get

        {

            return this.iva;

        }

        set

        {

            throw new Exception("La propiedad es de sólo lectura");

        }

    }

 

    public int idFactura

    {

        get

        {

            return this.idfactura;

        }

    }

 

    public string Nombre

    {

        get

        {

            return this.nombre;

        }

        set

        {

            throw new Exception("La propiedad es de sólo lectura");

        }

    }

 

    public string Ape1

    {

        get

        {

            return this.ape1;

        }

        set

        {

            throw new Exception("La propiedad es de sólo lectura");

        }

    }

 

    public string Ape2

    {

        get

        {

            return this.ape2;

        }

        set

        {

            throw new Exception("La propiedad es de sólo lectura");

        }

    }

 

    public int idCliente

    {

        get

        {

            return this.idcliente;

        }

    }

 

    public override string ToString()

    {

        string sp=" ";

 

        return this.numero + sp + this.fecha.ToString() +

            sp + this.nombre + sp + this.ape1 + this.ape2;

    }

}

 

Tenemos aquí varias cuestiones muy relevantes. En primer lugar, esta clase implementa las dos interfaces que definimos anteriormente, esto es, IFactura e ICliente. El motivo es que, recordemos, esta será la clase a la que pertenezcan los elementos que se encuentren en la colección ListaFacturas. En otras palabras, estos objetos no serán editables desde la aplicación cliente, sino que sirven únicamente para proporcionar la información justa y necesaria de lo que hay en la base de datos. Así conseguiremos reducir al máximo el peso de la colección, puesto que la colección de detalles de cada factura, por ejemplo, no es relevante para este propósito. Otra cuestión es que las propiedades deben implementar los bloques set y get tal y como exigen las interfaces, pero en esta clase en concreto, para evitar tentaciones en la aplicación cliente, hemos anulado todos los bloques set lanzando una excepción indicando que es de sólo lectura. Por último, en esta clase hay solamente un constructor que es, además, internal. ¿Por qué? Pues porque esta clase no debe poderse instanciar directamente desde el cliente, sino que podrá estar solamente como miembro de su colección.

 

Continuando con la lista de facturas, decíamos que una buena idea es dar la posibilidad de que se pueda recorrer la colección mediante un bucle foreach. Para esto deberemos construir otra clase más que implemente la interface IEnumerator. La llamaremos ListaFacturasEnumerator:

 

public class ListaFacturasEnumerator:IEnumerator

{

    private ListaFacturas lista;

    private int pos=-1;

 

    public ListaFacturasEnumerator(ListaFacturas l)

    {

        this.lista=l;

    }

 

    public bool MoveNext()

    {

        if (this.pos<this.lista.Count-1)

        {

            this.pos++;

            return true;

        }

        else

            return false;

    }

 

    public void Reset()

    {

        this.pos=-1;

    }

 

    public object Current

    {

        get

        {

            return this.lista[this.pos];

        }

    }

}

 

Después explicaré el funcionamiento de esta clase, pues debe ir en conjunción con otra que implemente la interface IEnumerable. En nuestro caso, será la clase ListaFacturas:

 

public class ListaFacturas:IEnumerable

{

    private DataTable dt;

 

    public ListaFacturas()

    {

        dt=new DataTable();

        SqlDataAdapter da=new SqlDataAdapter("SELECT Facturas.*, Clientes.* "+

            "FROM Clientes INNER JOIN Facturas ON Clientes.idCliente=Facturas.idCliente",

            dbSrc.cnn);

        da.Fill(dt);

    }

 

    public ListaFacturas(int idCliente)

    {

        dt=new DataTable();

        SqlDataAdapter da=new SqlDataAdapter("SELECT Facturas.*, Clientes.* "+

            "FROM Clientes INNER JOIN Facturas ON Clientes.idCliente=Facturas.idCliente "+

            "WHERE Facturas.idCliente="+idCliente.ToString(),dbSrc.cnn);

        da.Fill(dt);

    }

 

    public FacturaEnListado this[int idx]

    {

        get

        {

            DataRow r=this.dt.Rows[idx];

            FacturaEnListado f=new FacturaEnListado(r ["Numero"].ToString(),

                System.DateTime.Parse(r["Fecha"].ToString()),

                System.Byte.Parse(r["IVA"].ToString()),

                System.Int32.Parse(r["idFactura"].ToString()),

                r["Nombre"].ToString(),r["Ape1"].ToString(),

                r["Ape2"].ToString(),

                System.Int32.Parse(r ["idCliente"].ToString()));

 

            return f;

        }

    }

 

    public int Count

    {

        get

        {

            return this.dt.Rows.Count;

        }

    }

 

    public IEnumerator GetEnumerator()

    {

        return new ListaFacturasEnumerator(this);

    }

}

 

Bien, como decía, esta clase implementa la interface IEnumerable, cuyo único miembro es el método GetEnumerator, que devuelve un objeto que implemente la clase IEnumerator. Lo que hacemos, por lo tanto, es devolver un nuevo objeto de la clase ListaFacturasEnumerator que construimos anteriormente. Podemos decir que escribir un bucle foreach, así:

 

ListaFacuras lista=new ListaFacturas();

 

foreach (FacturaEnListado f in lista)

{

    // Código del bucle usando el objeto f

}

 

Sería equivalente a escribir esto otro:

 

ListaFacturas lista=new ListaFacturas();

 

IEnumerator ie=lista.GetEnumerator();

while (ie.MoveNext())

{

    // Código del bucle usando ie.Current

}

 

En la clase ListaFacturas, como podéis ver, tenemos dos constructores: uno sin argumentos que recogerá la lista completa desde la base de datos, y otro con un argumento que recogerá las facturas pertenecientes al cliente cuyo id se pase en dicho argumento. Tenemos también un indizador, que nos devolverá el objeto de la clase FacturaEnListado que se especifique en el índice entre corchetes y, por último, la propiedad Count, que devolverá el número de facturas que tenemos en la colección. Fijaos en que tenemos "por debajo" un objeto DataTable privado, que será el que contenga el conjunto de filas que hayamos recogido de la base de datos, y que cada vez que se recoja alguno de los miembros de la colección (mediante el indizador) lo que se hace es crear y devolver un nuevo objeto de la clase FacturaEnListado con los datos de la fila solicitada (en la propiedad Rows del DataTable).

 

Pues bien, las clases para confeccionar la lista de clientes serán muy similares a estas, con la salvedad de que ahora ya no habrá que implementar la interface IFactura. Como las explicaciones dadas anteriormente sirven también para estas, las pondré todas seguidas:

 

public class ClienteEnListado:ICliente

{

    private string nombre="";

    private string ape1="";

    private string ape2="";

    private int idcliente=0;

 

    internal ClienteEnListado(string nombre, string ape1,

        string ape2, int idcliente)

    {

        this.nombre=nombre.Trim();

        this.ape1=ape1.Trim();

        this.ape2=ape2.Trim();

        this.idcliente=idcliente;

    }

 

    public string Nombre

    {

        get

        {

            return this.nombre;

        }

        set

        {

            throw new Exception("La propiedad es de sólo lectura");

        }

    }

 

    public string Ape1

    {

        get

        {

            return this.ape1;

        }

        set

        {

            throw new Exception("La propiedad es de sólo lectura");

        }

    }

 

    public string Ape2

    {

        get

        {

            return this.ape2;

        }

        set

        {

            throw new Exception("La propiedad es de sólo lectura");

        }

    }

 

    public int idCliente

    {

        get

        {

            return this.idcliente;

        }

    }

 

    public override string ToString()

    {

        string sp=" ";

 

        return this.nombre + sp + this.ape1 + sp + this.ape2;

    }

}

 

public class ListaClientesEnumerator:IEnumerator

{

    private ListaClientes lista;

    private int pos=-1;

 

    internal ListaClientesEnumerator(ListaClientes l)

    {

        this.lista=l;

    }

 

    public bool MoveNext()

    {

        if (this.pos<this.lista.Count-1)

        {

            this.pos++;

            return true;

        }

        else

            return false;

    }

 

    public void Reset()

    {

        this.pos=-1;

    }

 

    public object Current

    {

        get

        {

            return this.lista[pos];

        }

    }

}

 

public class ListaClientes:IEnumerable

{

    private DataTable dt;

 

    public ListaClientes()

    {

        dt=new DataTable();

        SqlDataAdapter da=new SqlDataAdapter("SELECT * FROM Clientes",dbSrc.cnn);

        da.Fill(dt);

    }

 

    public ClienteEnListado this[int idx]

    {

        get

        {

            DataRow r=this.dt.Rows[idx];

            ClienteEnListado c=new ClienteEnListado(r ["Nombre"].ToString(),

                r["Ape1"].ToString(), r["Ape2"].ToString(),

                System.Int32.Parse(r["idCliente"].ToString()));

 

            return c;

        }

    }

 

    public int Count

    {

        get

        {

            return this.dt.Rows.Count;

        }

    }

 

    public IEnumerator GetEnumerator()

    {

        return new ListaClientesEnumerator(this);

    }

}

 

Ahora vamos a comenzar con la clase Cliente. Esta clase debe permitir dos cosas: por un lado, crear nuevos registros en la tabla de clientes y, por otro, modificar registros existentes. Por este motivo escribiremos dos constructores: uno sin argumentos que se ocupará de crear un registro nuevo, y otro con un argumento para recibir el id del cliente cuyos datos se quieren modificar. Vamos con la clase Cliente completa, y después la comentamos con más detalle:

 

public class Cliente:ICliente

{

    internal DataTable dt;

    internal DataRow dr;

    internal SqlDataAdapter da;

 

    public Cliente()

    {

        dt=new DataTable("Clientes");

        da=new SqlDataAdapter("SELECT * FROM Clientes WHERE idCliente=0",dbSrc.cnn);

 

        da.MissingSchemaAction=MissingSchemaAction.AddWithKey;

        da.Fill(dt);

 

        this.AsignarTipos();

 

        SqlCommand cmdIns=new SqlCommand("InsertarCliente", dbSrc.cnn);

        cmdIns.CommandType=CommandType.StoredProcedure;

        da.InsertCommand=cmdIns;

 

        da.InsertCommand.Parameters.Add("@Nombre",

            SqlDbType.NChar,15,"Nombre");

        da.InsertCommand.Parameters.Add("@Ape1",

            SqlDbType.NChar,15,"Ape1");

        da.InsertCommand.Parameters.Add("@Ape2",

            SqlDbType.NChar,15,"Ape2");

   

        SqlParameter prm=da.InsertCommand.Parameters.Add(

            "@idCliente",SqlDbType.Int,0,"idCliente");

        prm.Direction=ParameterDirection.Output;

 

        dr=dt.NewRow();

        dr["Nombre"]="";

        dr["Ape1"]="";

        dr["Ape2"]="";

        dt.Rows.Add(dr);

 

        CrearComandoUpdate();

    }

 

    public Cliente(int idCliente)

    {

        dt=new DataTable("Clientes");

        da=new SqlDataAdapter("SELECT * FROM Clientes WHERE idCliente=" +

            idCliente.ToString(), dbSrc.cnn);

        da.Fill(dt);

 

        this.AsignarTipos();

 

        if (dt.Rows.Count==0)

            throw new Exception("No se ha encontrado el cliente");

        else

            dr=dt.Rows[0];

 

        CrearComandoUpdate();

    }

 

    private void CrearComandoUpdate()

    {

        SqlCommandBuilder cmd=new SqlCommandBuilder(da);

        da.UpdateCommand=cmd.GetUpdateCommand();

    }

 

    public void Guardar()

    {

        try

        {

            this.da.Update(this.dt);

        }

        catch

        {

            throw new Exception("Error de concurrencia. Algún listo ha tocado los datos mientras tú estabas modificando.");

        }

    }

 

    public string Nombre

    {

        get

        {

            return this.dr["Nombre"].ToString();

        }

        set

        {

            this.dr["Nombre"]=value;

        }

    }

 

    public string Ape1

    {

        get

        {

            return this.dr["Ape1"].ToString();

        }

        set

        {

            this.dr["Ape1"]=value;

        }

    }

 

    public string Ape2

    {

        get

        {

            return this.dr["Ape2"].ToString();

        }

        set

        {

            this.dr["Ape2"]=value;

        }

    }

 

    public int idCliente

    {

        get

        {

            return (int) this.dr["idCliente"];

        }

    }

 

    private void AsignarTipos()

    {

        foreach (DataColumn dc in this.dt.Columns)

        {

            if (dc.ColumnName=="idCliente")

                dc.DataType=Type.GetType("System.Int32");

            else

                dc.DataType=Type.GetType("System.String");

        }

    }

 

    public override string ToString()

    {

        string sp=" ";

        return this.Nombre + sp + this.Ape1 + sp + this.Ape2;

    }

}

 

Esta clase también implementa la interface ICliente aunque dicha implementación es distinta de la que hacen las clases ClienteEnListado y FacturaEnListado, lógicamente. Comenzaremos por estudiar los campos. Podéis apreciar que tenemos tres: un DataTable, un DataRow y un DataAdapter, y que el modificador de acceso de todos ellos es internal. ¿Para qué nos hacen falta estos tres campos? Pues muy sencillo: el DataTable tendrá abierta una consulta con un único registro que será el que corresponda al cliente que estamos utilizando. El DataRow será ese registro, precisamente, y el DataAdapter, es, como ya te supondrás, el que nos va a conectar con la base de datos, bien para recogerlos o bien para almacenar cambios.

 

Luego veis que tenemos dos constructores como decíamos anteriormente: el primero de ellos, sin argumentos, se ocupa de crear un nuevo registro en el DataTable, eso sí, con sus campos vacíos, lógicamente. ¿Cómo lo hace? Pues bien, vamos paso a paso:

 

En primer lugar instancia el DataTable y el DataAdapter. Dado que dicho DataTable, por un lado, solamente va a contener la fila con la que se esté trabajando y, por otro, aún no se ha creado dicha fila, la consulta de selección del DataAdapter debe retornar un conjunto de datos vacío. Por este motivo, la cláusula WHERE dice que recoja aquellos registros cuyo idCliente sea 0. Como, debido al diseño de la base de datos, sabemos que no habrá ninguno, pues conseguimos el conjunto de filas vacío que andábamos buscando. Después se invoca el método Fill y se asignan los tipos de cada campo (más que nada, para facilitarnos el trabajo a la hora de hacer asignaciones sin necesidad de utilizar el método Parse). Luego se asigna al DataAdapter un procedimiento almacenado en su propiedad InsertCommand, y se le especifican el tipo y la longitud de cada uno de sus parámetros. ¿Para qué necesitamos un procedimiento almacenado? La razón es muy sencilla: para conocer el valor del campo de identidad de la fila en cuestión cuando esta se añada a la base de datos. ¿Por qué necesitamos ese dato? Pues, ahora, la razón es muy poderosa: porque necesitaremos ese dato para poder almacenar después una factura de ese cliente, puesto que la relación está establecida con el campo de clave principal. El procedimiento almacenado en SQL Server, por lo tanto, debe ser este:

 

CREATE PROCEDURE dbo.InsertarCliente

                @Nombre nchar(15),

                @Ape1 nchar(15),

                @Ape2 nchar(15),

                @idCliente int OUT

AS

                INSERT INTO Clientes (Nombre, Ape1, Ape2)

                VALUES (@Nombre, @Ape1, @Ape2)

                SET @idCliente=SCOPE_IDENTITY()

GO

 

Fijaos en que el parámetro idCliente es de salida y no de entrada como los otros tres, y su valor se asigna en la instrucción INSERT, devolviendo el valor de identidad que le haya asignado el motor de base de datos. Posteriormente a esto se añade un nuevo registro en el DataTable con los campos en blanco (evidentemente, porque es un cliente nuevo), y después se invoca el método privado CrearComandoUpdate el cual, sencillamente, obtiene el comando de actualización de un objeto SqlCommandBuilder y lo pone en la propiedad UpdateCommand del DataAdapter.

 

A partir de aquí, gracias a este constructor, la creación de nuevos registros en la tabla de clientes por parte de cualquier aplicación de cualquier tipo que trabaje con esta biblioteca de clases será de lo más sencilla. Bastaría este código para crear un nuevo registro:

 

Cliente c=new Cliente();

c.Nombre="Pepe";

c.Ape1="Pótamo";

c.Ape2="Piscinas";

 

Lo cual, como veis, es bastante más cómodo, sobre todo si pensamos en que, probablemente, tengamos que construir varias aplicaciones distintas contra el mismo origen de datos, porque nos basta con escribir una única vez todo el código que se ocupa de esto.

 

El otro constructor se ocupa de recoger de la base de datos el registro del cliente cuyo id coincida con el que le hemos pasado en su único argumento y, de este modo, podamos modificarlo con toda comodidad. Fijaos en que empieza de un modo similar al constructor anterior, esto es, instancia el DataTable y del DataAdapter, pero este último lo hace de un modo distinto. Prestad atención a la cláusula WHERE y veréis la diferencia: en efecto, ahora buscamos el registro cuyo idCliente coincida con el que nos han pasado en el argumento. A partir de aquí la cosa cambia: ya no asignamos ningún comando Insert al DataAdapter. ¿Por qué? Pues porque no lo necesitamos: ya que nos pasan el id de un cliente, será ese registro el que encapsule esta clase, de modo que no hay que crear registros nuevos (y tampoco hay que permitirlo). Solamente hay que comprobar si, efectivamente, el cliente que se nos pide existe en la base de datos. En caso de que no existiera, como veis, hay que lanzar una excepción.

 

El método guardar se ocupa de guardar físicamente los cambios en la base de datos (tanto si es un registro nuevo como si era un registro antiguo). Como veis, se hace una llamada al método Update del DataAdapter, y listo. ¿Por qué hemos encerrado esta llamada en un bloque try? Pues para detectar posibles errores de concurrencia. Decíamos que el DataTable, al igual que el DataSet, trabajaba de un modo completamente desconectado. Pues bien, si alguien modifica o borra un registro que yo tenía cargado en el DataTable se producirá un error de concurrencia cuando yo intente guardar los cambios. ¿Cómo se puede arreglar esto? Pues con mucho cuidado (jeje, vaya una respuesta...). Tenemos dos opciones: una es avisar al usuario del problema (pues vaya solución...) y, la otra, es arreglarlo. Dependiendo de las circunstancias y de la aplicación cliente que estemos diseñando, habrá que optar por una o por otra. Por esta razón no debe ser la biblioteca la que tome esta decisión, sino que tiene que ser la aplicación cliente. ¿Y cómo podríamos arreglar el problema desde la aplicación cliente? Pues con unos bloques try bien anidaditos, así:

 

Cliente c2=null;

try

{

    c.Guardar();

}

catch

{

    try

    {

        c2=new Cliente(c.idCliente);

        c2.Nombre=c.Nombre;

        c2.Ape1=c.Ape1;

        c2.Ape2=c.Ape2;

        c2.Guardar();

    }

    catch

    {

        c2=new Cliente();

        c2.Nombre=c.Nombre;

        c2.Ape1=c.Ape1;

        c2.Ape2=c.Ape2;

        c2.Guardar();

    }

    finally

    {

        c=c2;

    }

}

Es decir, primero se intenta guardar el cliente c. En caso de error, creamos un nuevo objeto Cliente, cargando de la base de datos aquél cuyo id sea el mismo que el que teníamos. Después modificamos sus datos y lo volvemos a guardar. Si esto produce un nuevo error, ya sabemos lo que ha ocurrido: se eliminó el registro mientras nosotros lo teníamos en la memoria, de modo que creamos uno nuevo con los mismos datos y lo guardarmos. El bloque finally depende del segundo try, y lo que hace es asignar a c (que era el objeto original que estábamos usando) la referencia de c2, de modo que c tenga ya los datos que se guardaron correctamente. Como veis, es bastante más sencillo que andar creando nuevos objetos DataTable y DataAdapter, y volver a asignar comandos Insert o Update. Como todo eso lo tenemos encapsulado, una vez más, nos ahorramos un montón de trabajo en la aplicación cliente.

 

Poco más tiene ya la clase Cliente. Simplemente las propiedades, cuyos bloques get y set devuelven lo que haya en el campo correspondiente del DataRow y asignan el nuevo valor en dicho campo, respectivamente. El método ToString, sobreescrito es claramente mejorable, pero basta y sobra para hacer pruebas con una aplicación cliente de Consola.

 

Antes de ir con la clase Factura, os voy a responder a la pregunta que, seguramente, estará rondando vuestras mentes. ¿Cómo puedo recoger el valor de identidad de un registro en Access, dado que este no soporta procedimientos almacenados? Bien, en este caso habrá que utilizar el evento RowUpdated. Aparte, lógicamente, no podemos utilizar objetos Sql, sino que habrá que utilizar los OleDb. Pues bien, en dicho procedimiento de evento, habría que construir un objeto OleDbCommand con la instrucción: "SELECT @@IDENTITY", y después asignar al campo de clave principal lo que devuelva el método ExecuteScalar de dicho objeto Command. Algo así:

 

protected static void Actualizando(object sender, OleDbRowUpdatedEventArgs e)

{

    OleDbCommand cmdIdentidad = new OleDbCommand("SELECT @@IDENTITY", dbSrc.cnn);

 

    if (e.StatementType == StatementType.Insert)

        e.Row["idCliente"] = (int) cmdIdentidad.ExecuteScalar();

}

 

Ahora sí, vamos ya con la clase Factura:

 

public class Factura:IFactura

{

    private SqlConnection cnn;

    private DataSet ds;

    private DataTable dtCliente;

    private DataTable dtFactura;

    private DataTable dtDetalles;

    private SqlDataAdapter daCliente;

    private SqlDataAdapter daFactura;

    private SqlDataAdapter daDetalles;

    private DataRow drCliente;

    private DataRow drFactura;

    protected Cliente DatosCliente;

    protected Detalles DetallesFactura;

 

    public Factura()

    {

        // Creando conexión para esta Factura

        cnn=dbSrc.cnn;

 

        // Creación del DataSet de esta Factura

        ds=new DataSet();

 

        // Creación del nuevo cliente

        this.DatosCliente=new Cliente();

 

        daCliente=this.DatosCliente.da;

        dtCliente=this.DatosCliente.dt;

        drCliente=this.DatosCliente.dr;

 

        // Creación del DataAdapter para la factura

        daFactura=new SqlDataAdapter("SELECT * FROM Facturas WHERE idFactura=0",cnn);

 

        // Creación del DataAdapter para los detalles

        daDetalles=new SqlDataAdapter("SELECT * FROM Detalles WHERE idDetalle=0",cnn);

        this.PrepararDetalles();

 

        /* Llamamos al método RellenarDataSet, que rellena ds

         * con las tablas de Clientes y Facturas, asigna los

         * tipos de los campos y crea las relaciones entre las

         * tablas    */

        this.RellenarDataSet();

 

        this.FacturaNueva();

 

        // Preparamos los comandos Update para los DataAdapter

        this.CrearComandosUpdate();

 

        // Creamos la colección de detalles

        this.DetallesFactura=new Detalles(dtDetalles, drFactura);

    }

 

    public Factura(int idFactura)

    {

        cnn=dbSrc.cnn;

        this.ds=new DataSet();

 

        // Creación del DataAdapter daFactura

        daFactura=new SqlDataAdapter("SELECT * FROM Facturas WHERE idFactura="+idFactura.ToString(),cnn);

 

        // Rellenando DataSet

        daFactura.Fill(ds, "Facturas");

 

        // Comprobando si se ha hallado la factura de marras

        dtFactura=ds.Tables["Facturas"];

 

        if (dtFactura.Rows.Count==0)

            throw new Exception("No se ha encontrado la factura");

        else

            drFactura=dtFactura.Rows[0];

 

        // Creación del cliente

        this.DatosCliente=new Cliente(Int32.Parse(drFactura["idCliente"].ToString()));

 

        daCliente=this.DatosCliente.da;

        dtCliente=this.DatosCliente.dt;

        drCliente=this.DatosCliente.dr;

 

        ds.Tables.Add(dtCliente);

 

        // Creación del DataAdapter daDetalles

        daDetalles=new SqlDataAdapter("SELECT * FROM Detalles WHERE idFactura="+idFactura.ToString(),cnn);

        this.PrepararDetalles();

 

        // Rellenando el DataSet con los detalles

        daDetalles.Fill(ds,"Detalles");

 

        // Asignando la tabla a dtDetalles

        dtDetalles=ds.Tables["Detalles"];

 

        // Creando las relaciones

        this.CrearRelaciones();

 

        this.CrearComandosUpdate();

 

        // Creamos la colección de detalles

        this.DetallesFactura=new Detalles(dtDetalles, drFactura);

    }

 

    public Factura(ICliente c)

    {

        cnn=dbSrc.cnn;

        this.ds=new DataSet();

 

        // Creación del cliente

        this.DatosCliente=new Cliente(c.idCliente);

 

        daCliente=this.DatosCliente.da;

        dtCliente=this.DatosCliente.dt;

        drCliente=this.DatosCliente.dr;

 

        // Creación del DataAdapter para daFactura

        daFactura=new SqlDataAdapter("SELECT * FROM Facturas WHERE idFactura=0",cnn);

 

        // Creación del DataAdapter daDetalles

        daDetalles=new SqlDataAdapter("SELECT * FROM Detalles WHERE idDetalle=0",cnn);

        this.PrepararDetalles();

 

        // Rellenamos y completamos el DataSet

        this.RellenarDataSet();

 

        this.FacturaNueva();

 

        this.CrearComandosUpdate();

 

        // Creamos la colección de detalles

        this.DetallesFactura=new Detalles(dtDetalles, drFactura);

    }

 

    public string Numero

    {

        get

        {

            return this.drFactura["Numero"].ToString();

        }

        set

        {

            this.drFactura["Numero"]=value;

        }

    }

 

    public DateTime Fecha

    {

        get

        {

            return (DateTime) this.drFactura["Fecha"];

        }

        set

        {

            this.drFactura["Fecha"]=value;

        }

    }

 

    public byte IVA

    {

        get

        {

            return (byte) this.drFactura["IVA"];

        }

        set

        {

            this.drFactura["IVA"]=value;

        }

    }

 

    public decimal Base

    {

        get

        {

            decimal baseimp=0;

 

            foreach (Detalle d in this.Detalles)

            {

                baseimp+=d.Importe;

            }

            return baseimp;

        }

    }

 

    public decimal Cuota

    {

        get

        {

            return this.Base*this.IVA/100;

        }

    }

 

    public decimal Total

    {

        get

        {

            return this.Base+this.Cuota;

        }

    }

 

    public int idFactura

    {

        get

        {

            return (int) this.drFactura["idFactura"];

        }

    }

 

    public Cliente Cliente

    {

        get

        {

            return this.DatosCliente;

        }

    }

 

    public Detalles Detalles

    {

        get

        {

            return this.DetallesFactura;

        }

    }

 

    public void Guardar()

    {

        dbSrc.cnn.Open();

        SqlTransaction tr=dbSrc.cnn.BeginTransaction();

 

        if (this.daCliente.InsertCommand!=null)

            this.daCliente.InsertCommand.Transaction=tr;

 

        this.daCliente.UpdateCommand.Transaction=tr;

 

        if (this.daFactura.InsertCommand!=null)

            this.daFactura.InsertCommand.Transaction=tr;

 

        this.daFactura.UpdateCommand.Transaction=tr;

 

        this.daDetalles.InsertCommand.Transaction=tr;

        this.daDetalles.UpdateCommand.Transaction=tr;

        this.daDetalles.DeleteCommand.Transaction=tr;

   

        try

        {

            this.daCliente.Update(this.dtCliente);

            this.daFactura.Update(this.dtFactura);

            this.daDetalles.Update(this.dtDetalles);

            tr.Commit();

        }

        catch

        {

            tr.Rollback();

            throw new Exception("Error de concurrencia. Algún listo ha tocado los datos mientras tú estabas modificando.");

        }

        finally

        {

            dbSrc.cnn.Close();

        }

    }

 

    /// <summary>

    /// Prepara el DataAdapter daFactura para que trabaje

    /// en caso de que haya que crear un cliente nuevo

    /// </summary>

    private void FacturaNueva()

    {

        daFactura.MissingSchemaAction=MissingSchemaAction.AddWithKey;

        daFactura.Fill(ds,"Facturas");

 

        /* Creación del comando INSERT en daFactura a partir del

            * procedimiento almacenado InsertarFactura */

        SqlCommand cmdIns=new SqlCommand("InsertarFactura", cnn);

        cmdIns.CommandType=CommandType.StoredProcedure;

        daFactura.InsertCommand=cmdIns;

 

        // Creación de los parámetros para daFacturas

 

        // Añadiendo parámetros de entrada

        daFactura.InsertCommand.Parameters.Add("@Numero",

            SqlDbType.NChar,13,"Numero");

        daFactura.InsertCommand.Parameters.Add("@Fecha",

            SqlDbType.DateTime,0,"Fecha");

        daFactura.InsertCommand.Parameters.Add("@IVA",

            SqlDbType.SmallInt,0,"IVA");

        daFactura.InsertCommand.Parameters.Add("@idCliente",

            SqlDbType.Int,0,"idCliente");

   

        // Añadiendo el parámetro de salida @idFactura

        SqlParameter prm=daFactura.InsertCommand.Parameters.Add(

            "@idFactura",SqlDbType.Int,0,"idFactura");

        prm.Direction=ParameterDirection.Output;

 

        // Añadiendo el registro a la factura

        drFactura=dtFactura.NewRow();

        drFactura.SetParentRow(drCliente);

        drFactura["Numero"]="";

        drFactura["Fecha"]=DateTime.Today;

        drFactura["IVA"]=16;

        dtFactura.Rows.Add(drFactura);

    }

 

    private void PrepararDetalles()

    {

        daDetalles.MissingSchemaAction=MissingSchemaAction.AddWithKey;

   

        // Preparando comandos para los detalles

        SqlCommandBuilder cmd=new SqlCommandBuilder(daDetalles);

 

        daDetalles.InsertCommand=cmd.GetInsertCommand();

        daDetalles.UpdateCommand=cmd.GetUpdateCommand();

        daDetalles.DeleteCommand=cmd.GetDeleteCommand();

    }

 

    private void CrearComandosUpdate()

    {

        SqlCommandBuilder cmd=new SqlCommandBuilder(daFactura);

        daFactura.UpdateCommand=cmd.GetUpdateCommand();

    }

 

    private void RellenarDataSet()

    {

        // Rellenando el DataSet con clientes, facturas y detalles

        ds.Tables.Add(dtCliente);

        daFactura.Fill(ds,"Facturas");

        daDetalles.Fill(ds,"Detalles");

 

        // Asignando objetos DataTable a las tablas del DataSet

        dtFactura=ds.Tables["Facturas"];

        dtDetalles=ds.Tables["Detalles"];

 

        // Asignando tipos a los campos

        this.AsignarTipos(dtCliente, dtFactura, dtDetalles);

 

        this.CrearRelaciones();

    }

 

    private void CrearRelaciones()

    {

        DataRelation rel;

 

        // Creando la relación entre Clientes y Facturas

        rel=new DataRelation("ClientesFacturas",

            dtCliente.Columns["idCliente"],

            dtFactura.Columns["idCliente"]);

        ds.Relations.Add(rel);

 

        // Creando la relación entre Facturas y Detalles

        rel=new DataRelation("FacturasDetalles",

            dtFactura.Columns["idFactura"],

            dtDetalles.Columns["idFactura"]);

        ds.Relations.Add(rel);

    }

 

    private void AsignarTipos(DataTable dtCli, DataTable dtFac, DataTable dtDet)

    {

        foreach (DataColumn dc in dtCli.Columns)

        {

            if (dc.ColumnName=="idCliente")

                dc.DataType=Type.GetType("System.Int32");

            else

                dc.DataType=Type.GetType("System.String");

        }

 

        foreach (DataColumn dc in dtFac.Columns)

        {

            switch (dc.ColumnName)

            {

                case "Numero":

                    dc.DataType=Type.GetType("System.String");

                    break;

                case "Fecha":

                    dc.DataType=Type.GetType("System.DateTime");

                    break;

                case "IVA":

                    dc.DataType=Type.GetType("System.Byte");

                    break;

                default:

                    dc.DataType=Type.GetType("System.Int32");

                    break;

            }

        }

 

        foreach (DataColumn dc in dtDet.Columns)

        {

            switch (dc.ColumnName)

            {

                case "Descripcion":

                    dc.DataType=Type.GetType("System.String");

                    break;

                case "Precio":

                    dc.DataType=Type.GetType("System.Decimal");

                    break;

                default:

                    dc.DataType=Type.GetType("System.Int32");

                    break;

            }

        }

    }

 

    public override string ToString()

    {

        string sp="\n";

        string ret="Cliente: " + this.DatosCliente.ToString() + sp;

        ret+="Numero: " + this.Numero + sp;

        ret+="Fecha:" + this.Fecha.ToShortDateString();

 

        return ret;

    }

}

 

Esta clase implementa únicamente la interface IFactura. Os habréis fijado en que es mucho más extensa que la clase Cliente. La razón es que esta clase debe contener no sólo los datos de la factura en sí, sino también los del cliente de dicha factura y la colección de detalles. Por este motivo tenemos unos cuantos objetos DataTable, DataAdapter y DataRow. Concretamente, necesitamos un DataTable, un DataAdapter y un DataRow para el cliente y otro para la Factura, así como un DataTable y un DataAdapter para la colección de detalles. Además, aquí tenemos también un DataSet, que será el contenedor de todos estos DataTable. Por último, tenemos dos campos más, con nivel de acceso protected: DatosCliente, que será un objeto de la clase Cliente; y DetallesFactura, que será la colección de detalles.

 

En este caso tenemos tres constructores, que funcionan de un modo similar a cómo funcionaban los constructores de la clase Cliente. El primero de ellos no tiene argumentos, de modo que se ocupa de crear una factura nueva de un cliente también nuevo. Para ello, igual que la clase cliente, crea un nuevo registro en blanco en la tabla de facturas, así como otro registro en blanco en la tabla de clientes. El segundo recibe como argumento un id de factura, de modo que se ocupa de abrir una factura de la base de datos para que esta pueda ser modificada. Hasta aquí, el funcionamiento es similar al de la clase Cliente. Sin embargo, el tercer constructor recibe como argumento cualquier objeto que implemente la interface ICliente, y se ocupa de crear una factura nueva para el cliente que se le haya pasado. ¿Por qué cualquier objeto que implemente ICliente, y no un objeto de la clase Cliente? Pues por una razón de flexibilidad. Dado que hemos ofrecido un par de colecciones (ListaFacturas y ListaClientes) cuyos elementos implementaban ICliente, lo mejor es dar la posibilidad de crear las facturas partiendo de cualquier miembro de estas listas, así como también de algún cliente que hayamos creado con antelación. Por lo tanto, este constructor crea un registro nuevo en la tabla de facturas, pero asigna a su campo DatosCliente un objeto de la clase cliente a partir del que recibe como argumento.

 

Ni que decir tiene que aquí, del mismo modo que lo hicimos en la clase Cliente, también se asigna un procedimiento almacenado al comando Insert del DataAdapter para la factura, pues también necesitaremos el valor del campo de identidad una vez que el registro sea guardado en la base de datos. Aunque sea un poquito repetitivo, este es el procedimiento almacenado para este propósito:

 

CREATE PROCEDURE dbo.InsertarFactura

      @Numero nchar(12),

      @Fecha datetime,

      @IVA smallint,

      @idCliente int,

      @idFactura int OUT

AS

      INSERT INTO Facturas (Numero, Fecha, IVA, idCliente)

      VALUES (@Numero, @Fecha, @IVA, @idCliente)

      SET @idFactura=SCOPE_IDENTITY()

GO

 

Sin embargo, hay una cosa que hacen estos tres constructores y que no hacen los constructores de la clase Cliente, y es rellenar un DataSet con las tablas que almacenan los datos de la factura, así como las relaciones entre ellas. Por otra parte, si os fijáis, cuando se crea un objeto Factura (independientemente del constructor que se utilice), a los objetos DataTable y DataAdapter declarados para alojar los datos del cliente y los detalles se les asignan las referencias de los objetos DataTable y DataAdapter (con modificador de acceso internal) en los objetos DatosCliente y DetallesFactura. Así evitamos construir en la memoria varios objetos idénticos (esto está marcado en negrilla en el código). Por último, al añadir un registro para la factura se le asigna también su registro relacionado en la tabla de Clientes, utilizando el método SetParentRow (esto está marcado en amarillo en el código).

 

La última cosa importante a destacar de esta clase es el método Guardar. En este caso hemos abierto la conexión manualmente. ¿Por qué? Porque de lo contrario no podríamos usar una transacción. ¿Por qué necesitamos una transacción? Porque es imprescindible que la factura se guarde al completo, o no se guarde nada. Es decir, antes de nada hay que guardar el cliente, luego la factura y después los detalles, pues es así como nos lo exigen las relaciones existentes entre las tablas. Si no usáramos una transacción podría ocurrir que después de haber guardado el cliente y la factura se produjera un error al intentar guardar los detalles, con lo cual, la factura se habría guardado a medias. Con la transacción conseguiremos que se guarde todo correctamente o no se guarde nada. Las transacciones se crean con objetos SqlTransaction creados a partir del método BeginTransactionn del objeto SqlConnection. Pues bien, después de asignar dicha transacción a cada uno de los posibles comandos de cada uno de los DataAdapter, es cuando invocamos los métodos Update de cada uno. Como veis, primero guardamos el cliente, luego la factura y, por último, los detalles. Si todo va bien, ejecutamos el método Commit del objeto SqlTransaction. En caso de que se produzca algún error, ejecutamos el método RollBack para anular la transacción y lanzamos una excepción. En cualquier caso, esto es, tanto si se produce un error como si no se produce, tenemos que cerrar la conexión, motivo por el cual invocamos el método Close dentro del bloque finally.

 

Lo último que nos queda por ver es la implementación de las clase Detalle, DetallesEnumerator y la colección Detalles. Comenzaremos con la clase Detalle:

 

public class Detalle

{

    internal DataRow dr;

 

    internal Detalle(DataRow dr)

    {

        this.dr=dr;

    }

 

    public int Cantidad

    {

        get

        {

            return (int) this.dr["Cantidad"];

        }

        set

        {

            this.dr["Cantidad"]=value;

        }

    }

 

    public string Descripcion

    {

        get

        {

            return this.dr["Descripcion"].ToString();

        }

        set

        {

            this.dr["Descripcion"]=value;

        }

    }

 

    public decimal Precio

    {

        get

        {

            return (decimal) this.dr["Precio"];

        }

        set

        {

            this.dr["Precio"]=value;

        }

    }

 

    public decimal Importe

    {

        get

        {

            return (decimal) this.Cantidad * this.Precio;

        }

    }

 

    public int idDetalle

    {

        get

        {

            return (int) this.dr["idDetalle"];

        }

    }

 

    public override string ToString()

    {

        string sp=" ";

        return this.Cantidad.ToString() + sp + this.Descripcion

            + sp + this.Precio.ToString() + sp

            + this.Importe.ToString();

    }

}

 

¿Por qué hay un sólo constructor, y además es internal? Porque no queremos que esta clase se pueda instanciar de un modo aislado. Dicho de otro modo, los detalles solamente podrán estar dentro de una factura. Lo que recibe dicho constructor es el DataRow que contiene los datos de la línea de detalle que representa y nada más. Después se implementan sus propiedades y andando. Evidentemente, dado que no se puede crear un detalle aislado de una factura, no tendría ningún sentido un método guardar.

 

Decíamos que siempre es bueno que las colecciones se puedan iterar por medio de un bucle foreach, de modo que tenemos que crear también una clase DetallesEnumerator que implemente IEnumerator. Aquí está:

 

public class DetallesEnumerator:IEnumerator

{

    private int pos=-1;

    private Detalles dt;

 

    internal DetallesEnumerator(Detalles dt)

    {

        this.dt=dt;

    }

 

    public bool MoveNext()

    {

        if (pos < dt.NumDetalles-1)

        {

            pos++;

            return true;

        }

        else

            return false;

    }

 

    public void Reset()

    {

        pos=-1;

    }

 

    public object Current

    {

        get

        {

            return this.dt[pos];

        }

    }

}

 

No creo que hagan falta más comentarios sobre esta clase, porque es más o menos lo mismo que las clases ListaClientesEnumerator y ListaFacturasEnumerator. Vamos, pues, con la clase Detalles (o sea, la colección de detalles):

 

public class Detalles:IEnumerable

{

    internal DataTable dtDetalles;

    internal DataRow drFacturaPadre;

 

    internal Detalles(DataTable dt, DataRow drPadre)

    {

        this.dtDetalles=dt;

        this.drFacturaPadre=drPadre;

    }

 

    /* Proporcionamos un indizador de sólo lectura para que

     * se pueda acceder a los detalles por su índice    */

    public Detalle this[int idx]

    {

        get

        {

            if (this.dtDetalles.Rows.Count==0)

                throw new Exception("La colección de detalles está vacía");

            else if (idx>=this.dtDetalles.Rows.Count || idx < 0)

                throw new Exception("No existe ese detalle en la factura");

            else

            {

                DataRow r=this.dtDetalles.Rows[idx];

                return new Detalle(r);

            }

        }

    }

 

    public Detalle NuevoDetalle()

    {

        DataRow r=this.dtDetalles.NewRow();

        r["Cantidad"]=0;

        r["Descripcion"]="";

        r["Precio"]=0;

 

        return new Detalle(r);

    }

 

    public void Add(Detalle d)

    {

        d.dr.SetParentRow(this.drFacturaPadre);

        this.dtDetalles.Rows.Add(d.dr);

    }

 

    public System.Collections.IEnumerator GetEnumerator()

    {

        return new DetallesEnumerator(this);

    }

 

    public int NumDetalles

    {

        get

        {

            return this.dtDetalles.Rows.Count;

        }

    }

}

 

Del mismo modo que la clase Detalle, la colección Detalles tampoco puede ser instanciada fuera de una factura, motivo por el cual su único constructor es también internal. En esta ocasión, el constructor recibe el DataTable que contiene todos los registros de esta colección y un DataRow, que será el registro relacionado en la tabla de facturas. A partir de aquí, todo lo que debemos ofrecer es un indizador que permita acceder a los elementos de la colección por su índice, una propiedad que nos devuelva el número de elementos que tenemos en la colección (NumDetalles) y un par de métodos para poder añadir nuevos registros: NuevoDetalle, que se ocupa de crear el registro con los campos en blanco y devolver el objeto Detalle a la aplicación cliente para que esta escriba los valores; y el método Add, que será el que añada dicho registro al DataTable. Ya está todo.

 

Espero que la extensión del artículo no haya terminado por dejarte más dudas que cuando empezaste a leerlo. La encapsulación correcta del acceso a un origen de datos es una técnica cuyo dominio lleva cierto tiempo, pero las ventajas que ofrece al final son tan grandes que el esfuerzo merece muchísimo la pena.

 

Recuerda siempre que no se puede encapsular un acceso a datos de cualquier manera. La biblioteca debe ser robusta, flexible y eficiente. Por lo tanto, debes evitar siempre, en la medida de lo posible, el uso de colecciones demasiado pesadas. Aprovecha también las características orientadas a objetos del lenguaje, tales como el uso de interfaces, herencia y polimorfismo. Además, debes proporcionalidad toda la funcionalidad que sea necesaria del modo más intuitivo posible.

 

Sigue este vínculo para bajarte la biblioteca en C#. Para poder utilizarlo necesitarás una base de datos como la que hemos utilizado aquí, y hecha en SQL Server. Seguramente, también tendrás que modificar el nombre del servidor en la cadena de conexión. Recuerda que es una biblioteca de clases, de modo que tendrás que diseñar una aplicación cliente que la referencie para ver cómo funciona (dicha aplicación puede ser cualquiera: de consola, de Windows, Web ASP.NET, un servicio...)

 

No he implementado la biblioteca en Visual Basic.NET, pero la idea es la misma. Quizá lo haga y lo publique dentro de un tiempo. Hasta entonces, podrías probar a hacerlo tú mismo, pues facilitará mucho que puedas asentar los conocimientos.

 


la Luna del Guille o... el Guille que está en la Luna... tanto monta...