Capítulo 4.

Programación en ADO
4.1. Las bases de datos relacionales (RDBMS)
Una base de datos relacional es un conjunto de tablas que mantienen algún tipo de relación entre ellas. Una base de datos es un conjunto de información que se organiza por mediación de una estructura lógica. La información puede ser guardada en un fichero o en varios ficheros, también conocidos como tablas. 4.1.1. Las Tablas Dentro de la base de datos, las tablas son el objeto más básico e importante. El rendimiento de la base de datos y por extensión de las aplicaciones que accedan a ellas, dependen en gran medida del diseño de las tablas. Una tabla es una matriz bidimensional, que contiene filas y columnas. Las filas son los registros de la tabla y las columnas son los campos de la tabla. A las filas también se les conoce con el nombre de tuplas o registros. Un campo es una forma de decir que tipo de información contendrán las filas en esa posición. Por ejemplo una columna llamada Nombre, podrá contener el nombre de las personas.
Campos de la Tabla: Nombre, Apellidos y DNI

Estructura de la Tabla:
Nombre Antonio Luis María Apellidos Pérez López Martínez Sánchez García Hernández DNI 7328714K 3858582L 4738270X
Filas de la tabla o registros que contienen los datos organizados por las columnas de la tabla, también llamadas campos.

Las tablas de datos se pueden relacionar entre sí, generando bases de datos relacionales. En el mercado, existen varios sistemas administradores de bases de datos relacionales, como son: Access, SQL Server, MySQL, Oracle, SysBase, Informix, etc. Una tabla suele ser la representación de clases de objetos físicos, como por ejemplo, clientes, empleados, facturas, etc. Cada objeto físico, como un empleado, tiene su registro correspondiente en la tabla.

En esta imagen podemos ver una tabla de empleados con 7 campos.
Capítulo 4. Programación en ADO. Página 1

En este caso el campo ID de la tabla, es el campo Clave de la tabla, lo que significa que la tabla es accedida directamente por este campo y permite identificar a ese registro de forma inequívoca sobre cualquier otro registro de la tabla. El campo clave, ID de la tabla Empleados, permite que esta tabla se relacione con otras tablas de la base de datos, como por ejemplo Pedidos, o Privilegios de Empleados. 4.1.2. Las Claves. Existen distintos tipos de clave: Clave única. Cada tabla puede tener uno o más campos cuyos valores identifican de forma única cada registro de dicha tabla, es decir, no pueden existir dos o más registros diferentes cuyos valores en dichos campos sean idénticos. Este conjunto de campos se llama clave única. Pueden existir varias claves únicas en una determinada tabla, y a cada una de éstas suele llamársele candidata a clave primaria. Clave primaria. Una clave primaria es una clave única elegida entre todas las candidatas que define unívocamente a todos los demás atributos de la tabla, para especificar los datos que serán relacionados con las demás tablas. La forma de hacer esto es por medio de claves foráneas. Sólo puede existir una clave primaria por tabla y ningún campo de dicha clave puede contener valores NULL. Clave externa o foránea. Una clave externa es una referencia a una clave en otra tabla. Las claves externas no necesitan ser claves únicas en la tabla donde están y sí a donde están referenciadas. Por ejemplo, el código de departamento puede ser una clave externa en la tabla de empleados, obviamente se permite que haya varios empleados en un mismo departamento, pero existirá sólo un departamento. El objetivo en el diseño de las tablas es evitar la redundancia de los datos, es decir, imaginemos que la dirección del empleado apareciera en más de una tabla como contenido de un campo. En ese caso, hablamos de redundancia. La redundancia ralentiza la ejecución de la base de datos, así como genera un exceso de espacio en disco que, con una buena planificación en las relaciones entre tablas, se podría solucionar. La forma de evitarlo, es crear por ejemplo una tabla con las direcciones de los empleados y usar un campo clave para acceder a ellas. A parte de la clave primaria, podemos definir claves secundarias, que son claves que se utilizan para poder relacionar de una forma más precisa las tablas. El hecho de hace coincidir una clave secundaria con un valor de clave primaria, se denomina operación de búsqueda.
Capítulo 4. Programación en ADO. Página 2

Las claves primarias suelen ser campos numéricos de autoincremento, llamados Autonuméricos en Access e Identify en SQL Server. El uso de valores de campos alfabéticos como Clave Principal, ha caído en desuso por parte de los diseñadores de bases de datos, debido a su lentitud a la hora de realizar operaciones, pero muchas veces es la mejor o única opción. Otro método para generar claves exclusivas es utilizar los identificadores globales exclusivos (GUID). Los GUID son números binarios de 16 bytes cuya exclusividad está garantizada local y universalmente; ningún otro ordenador del mundo puede duplicar un GUID. En SQL Server, uniqueidentifier, es un tipo GUID. Por mediación de las claves podemos crear relaciones que agilizan las consultas y el acceso a los datos. Una tabla puede tener múltiples relaciones con otras tablas. El tipo de relaciones posibles que se pueden implementar en una base de datos son: Uno a Varios. Representa una relación entre un solo valor de clave principal (“uno”) y varias instancias del mismo valor en el campo clave secundario (“varios”). Las relaciones uno a varios son representadas como un 1 y el símbolo de infinito. Uno a Uno. Son relaciones en las que se conectan los valores de clave principal de dos tablas. Es una relación poco usada. Varios a Varios. Son relaciones que requieren tres tablas, una de las cuales es la tabla de vinculación. La tabla de vinculación debe de tener dos claves secundarias, cada una con una relación varios a uno con una clave principal de dos tablas relaciones. Estas relaciones también se las conoce como indirectas. Ejemplo de Esquema de las relaciones de una Base de Datos:

Capítulo 4. Programación en ADO.

Página 3

4.1.3. Las Clave Índice Las claves índices surgen con la necesidad de tener un acceso más rápido a los datos. Los índices pueden ser creados con cualquier combinación de campos de una tabla. Las consultas que filtran registros por medio de estos campos, pueden encontrar los registros de forma no secuencial usando la clave índice. Los índices generalmente no se consideran parte de la base de datos, pues son un detalle agregado. Un índice es un objeto que existe sólo dentro del marco de una determinada tabla o vista. Un índice funciona de una forma análoga al índice de un libro, donde existe algún tipo de valor (“clave”) de búsqueda que se organiza de una forma y, posteriormente, se proporciona otra clave con la que podremos encontrar la información que estamos buscando. Un índice nos proporciona métodos para agilizar la búsqueda de nuestra información. Los índices se organizan en dos categorías: Agrupados. Sólo puede haber un índice agrupado por tabla. Tener un índice agrupado significa que la tabla que se basa en dicho índice agrupado está ordenada físicamente según dicho índice. En un libro, el índice agrupado sería el número de página; la información se guarda en el orden de los números de página. No Agrupados. Se pueden tener tantos índices no agrupados como se quiera en la tabla. Este tipo de índice apunta a otro valor que nos va a permitir encontrar el dato. En nuestro ejemplo, serían los conceptos detallados en el índice del libro. 4.1.4. Los Desencadenadores Un desencadenador es un objeto que existe sólo dentro del marco de una tabla. Los desencadenadores son elementos de código lógico que se ejecutan automáticamente cuando se producen diversos eventos en nuestra tabla, como inserciones, actualizaciones o eliminaciones. Los desencadenadores se pueden utilizar para diversas cosas, pero principalmente se utilizan para copiar datos a medida que se introducen o para comprobar la actualización para asegurarse de que se satisfacen algunos criterios. 4.1.5. Restricciones Una restricción es otro objeto que sólo existe dentro de los confines de una tabla. Las restricciones confinan los datos de nuestra tabla para satisfacer determinadas condiciones. En cierto sentido, las restricciones compiten con los desencadenadores como posibles soluciones para solucionar problemas de integridad de datos. Sin embargo, no son iguales; cada elemento tiene sus propias ventajas inconfundibles.
Capítulo 4. Programación en ADO. Página 4

La tabla es el elemento básico de la estructura en una base de datos. Evidentemente es el objeto más importante, pero existen otros que lo complementan y añaden la funcionalidad necesaria para sacar toda la potencia a un sistema de base de datos relacional. 4.1.6. Diagramas Un diagrama es una representación visual del diseño de la base de datos, incluyendo las diversas tablas, los nombres de columna de cada tabla y las relaciones entre las mismas. Un Diagrama de entidad-relación (ERD), es un diagrama en el que la base de datos se divide en dos partes: las entidades y las relaciones. 4.1.7. Vistas Una vista es algo parecido a una tabla virtual. Básicamente se utilizan como una tabla, pero no contienen ningún dato propio. En su lugar, una vista es simplemente una planificación previa de la asignación y representación de los datos guardados en tablas. El plan se guarda en la base de datos en forma de consulta. Esta consulta llama a los datos de algunas columnas (no tienen que ser todas) para su recuperación por parte de una o más tablas. Los datos recuperados pueden o no reunir unos criterios especiales (dependiendo de la definición de la vista) para mostrarse como datos en dicha vista. Podemos usar una vista para incluir la información que todo el mundo pueda ver, dado que las aplicaciones normalmente discriminan la información en función del nivel de acceso del usuario. Asimismo, se puede hacer una vista a la medida para que los usuarios no tengan que realizar búsquedas a través de información innecesaria. 4.1.8. Procedimientos Almacenados Un procedimiento almacenado sigue siendo básico para las funciones de programación en SQL Server. Un procedimiento almacenado es una serie de instrucciones Transact-SQL ordenadas e integradas en una sola unidad lógica. Su equivalente en C#, sería un método.

Capítulo 4. Programación en ADO.

Página 5

4.2. Uso básico de Transact-SQL o T-SQL Las bases de datos relacionales usan un lenguaje de programación conocido como Transact-SQL o T-SQL. T-SQL es un lenguaje estándar para trabajar con bases de datos relacionales y es soportado por la mayoría de los sistemas actuales. Los comandos o instrucciones más habituales en SQL se aplican para: Crear y borrar tablas; Insertar registros en la tabla; actualizar la información de los registros en la tabla; borrar un registro o grupo de registros y; seleccionar un registro o conjunto de registros. Estas opciones son realizadas por: Create, Drop, Insert, Update, Delete y Select, respectivamente. 4.2.1. La instrucción SELECT La instrucción Select y las estructuras utilizadas en ella, forman la base de todos los comandos que vamos a ver. Sintaxis: SELECT <lista de columnas> [ FROM <tabla o tablas de origen> ] [WHERE <condición restrictiva>] [GROUP BY <nombre de columna o expresión que utiliza una columna en la lista de selección>] [HAVING <condición restrictiva basada en los resultados de GROUP BY>] [ORDER BY <lista de columnas>] Para empezar vamos a utilizar el ejemplo Northwind suministrado como ejemplo de ayuda por Microsoft. Tanto esta base como Pubs, fueron diseñadas para su uso con Access y SQl Server 2000. A partir de la versión 2005 fueron sustituidas por AdventureWorks. Esta última base incorpora funcionalidades de SQL Server 2005 muy avanzadas que en ocasiones dificultan el aprendizaje del T-SQL. Por ello, Microsoft mantiene tanto Pubs como Northwind disponibles para SQL Server 2005 y SQL Server 2008. Como primera prueba de Select, usemos la siguiente sintaxis: SELECT * FROM employees El resultado de esta consulta nos dará todas las columnas (*) de la tabla employee y todos los registros que contenga. Si no hemos manipulado el ejemplo, deberá de dar como resultado nueve filas. El uso del * le indica al intérprete de SQL que nos devuelva todos los campos (columnas) que tiene la tabla employees. La clausula FROM, le indica cual es la tabla o tablas de las que tiene que tomar los datos.

Capítulo 4. Programación en ADO.

Página 6

Si quisiéramos obtener sólo las columnas de nombre y apellidos, utilizaremos la sentencia SELECT con esta estructura: SELECT lastname, firstname FROM employees Como podemos ver los campos que queremos obtener van separados por coma. Si ejecutamos la consulta veremos que siguen saliendo 9 filas pero sólo nos da las columnas LastName y FirstName. Una cosa a tener en cuenta con el SQL es que no distingue a la hora de escribir las sentencias entre mayúsculas y minúsculas. Por ejemplo: SELECT y Select son la misma cosa para el intérprete de SQL. Hay algunos editores de SQL que ponen las instrucciones en mayúsculas y otros en minúsculas, pero sólo tiene un efecto visual. La cláusula Where. Esta cláusula nos permite agregar a la consulta condiciones sobre los resultados deseados. Hasta ahora, sólo hemos obtenido el total de las filas que contiene la tabla. Por ejemplo, supongamos que queremos los empleados SELECT lastname, firstname FROM employees WHERE Title = 'Sales Representative' Como observaremos, ya no son 9 las filas afectadas, ahora son sólo seis. Hemos utilizado la columna Title, para restringir las filas que queríamos consultar. A continuación vemos una tabla con los operadores que podemos utilizar en una cláusula Where: Operador < > < > y ¡= <= >= = ¡> <! AND, OR, NOT Uso Menor que Mayor que Distinto de Menor ó Igual que Mayor ó Igual que Igual que No mayor que No menor que Valores lógicos booleanos estándar. Se pueden utilizar para combinar múltiples condiciones. NOT se evalúa primero, AND, después y por último OR. Podemos cambiar el orden de evaluación colocando paréntesis. La comparación es verdadera si valor se encuentra comprendido entre los valores facilitados. Utiliza los caracteres % y _ como
Página 7

BETWEEN

LIKE
Capítulo 4. Programación en ADO.

IN

ALL, ANY, SOME

EXISTS.

comodines. % indica que un valor de cualquier longitud puede reemplazar al %. _ indica que cualquier carácter puede reemplazar a este carácter. [ ] los corchetes indican que cualquier carácter único dentro de dichos corchetes es correcto, por ejemplo [az]. En este caso, cualquier carácter entre la a y la z es válido. ^ este carácter funciona como operador NOT indicando que se excluye el siguiente carácter. Devuelve true si el valor que se encuentra a la izquierda de la palabra clave IN se corresponde con cualquier valor de la lista proporcionada tras la palabra clave IN. Normalmente se utiliza en subconsultas. Devuelve true si alguno o todos los valores (dependiendo del elegido) en una subconsulta coinciden con la condición del operador de comparación (por ejemplo, <, >, = , >=). ALL indica que el valor tiene corresponderse con todos los valores del conjunto. ANY y SOME son equivalentes funcionales y se evaluarán como true si la expresión se corresponde con cualquier valor del conjunto. Devuelve true si la subconsulta devuelve, al menos, una fila.

Por ejemplo, si quisiéramos obtener todos los trabajadores cuya alta en la empresa sea entre 1990 y 1993, la consulta sería: SELECT * FROM employees WHERE hiredate BETWEEN '01-01-1990' AND '01-01-1993' El resultado de esta consulta es de tres filas afectadas. La cláusula Order By. En las consultas, puede ocurrir que los datos salgan ordenados alfabéticamente. Eso podría ser por casualidad. Si no le indicamos que deseamos ordenar los resultados de la consulta de una forma específica, se obtendrán los datos en la forma menos costosa para recopilar los datos.

Capítulo 4. Programación en ADO.

Página 8

Normalmente se basará, bien en el orden físico de una tabla o bien en uno de los índices de SQL Server utilizados para buscar los datos. Order By nos permite seleccionar cual es la columna o columnas que serán la base para ordenar los resultados de la consulta. Ejemplo: SELECT * FROM employees WHERE hiredate BETWEEN '01-01-1990' AND '01-01-1993' ORDER BY FirstName Si observamos esta consulta respecto a la anterior, observaremos que en esta ocasión los datos son mostrados en el orden alfabético de la columna FirstName. Si no queremos, no es necesario colocar una cláusula Where, pero en caso de ir, tiene que ir antes del Order By. Por ejemplo: SELECT * FROM employees ORDER BY FirstName, LastName En este ejemplo seleccionamos todos los empleados, pero mostrados en orden alfabético por el FirstName y después por el LastName. Evidentemente podemos aplicar ordenaciones por campos numéricos, incluso cambiar si es ascendente o descendente, por ejemplo: SELECT * FROM products WHERE unitsonorder > 0 AND unitsinstock < 10 ORDER BY unitsonorder DESC Este ejemplo, muestra todos los valores cuyo campo unitsonorder sea superior a cero y su campo unitsinstock se inferior a 10. El resultado es ordenador de forma descendente por unitsonorder. De forma implícita un Order By ejecuta la ordenación de forma ascendente. Podríamos indicárselo de forma explícita, colocando ASC. DESC le indica que la ordenación sea descendente. SELECT * FROM employees WHERE hiredate BETWEEN '01-01-1990' AND '01-01-1993' ORDER BY hiredate, employeeid DESC Nos muestra la consulta ordenada primeramente por la columna, hiredate, de forma descendente y si hubiera coincidencias, utilice la columna employeeid para ordenar. La cláusula Group By. Esta cláusula se utiliza para agregar información. Si realizamos la siguiente consulta, nos devolverá todos los artículos que se han pedido dentro de un conjunto concreto de pedidos.

Capítulo 4. Programación en ADO.

Página 9

SELECT orderid, quantity FROM [order details] WHERE orderid BETWEEN 11000 AND 11002 Como podemos observar al ejecutar la consulta, nos ha devuelto 11 filas de la tabla order details, que son las que coinciden con el orderid entre el 11000 y el 11002. Si observamos detenidamente, podemos ver que por ejemplo de la orden 11000 hay tres filas, lo que significa que aunque en realidad la orden era sólo para ver tres pedidos, la consulta nos ha devuelto el detalle individual de cada pedido. Cuatro filas para el pedido 11001 y 4 filas para el pedido 11002. Si quisiéramos ver sólo los pedidos, sin él detalle, podemos usar Group By y, además, aplicarle un agregado a la sentencia select, que en este caso, es la función de la suma sum(). Si ejecutamos la orden como sigue, obtendremos tres filas con el total del valor del pedido. SELECT orderid, SUM(quantity) FROM [order details] WHERE orderid BETWEEN 11000 AND 11002 GROUP BY orderid Sum() realiza la suma, pero si lo que necesitamos es el número de filas, podemos utilizar la función Count(). Ejemplo: SELECT customerid, employeeid, COUNT(*) FROM orders WHERE customerid BETWEEN 'A' AND 'B' GROUP BY customerid, employeeid Esta consulta obtiene todos los pedidos por clientes y por empleados que hicieron esos pedidos. El resultado es que para cada cliente nos dice el número de pedidos que gestiono los empleados. Al usar Group By, toda columna en la lista de selección tienen que formar parte de Group By o tiene que ser un agregado. Agregados. Un agregado es una función que actúa sobre grupos de datos. En el ejemplo que usamos el agregado SUM(), lo que obtuvimos fue la suma de la columna quantity. La suma se calcula y se devuelve sobre la columna seleccionada para cada grupo definido en la cláusula GROUP BY(en ese caso, sólo OrderId). Hay muchas clases de agregados, pero sólo vamos a estudiar las más usuales: sum, count, avg (media), min, max. AVG. Realiza una media de los valores. Por ejemplo: SELECT orderid, AVG(quantity) FROM [order details] WHERE orderid BETWEEN 11000 AND 11002 GROUP BY orderid Esta instrucción ha calculado el importe medio para cada pedido.

Capítulo 4. Programación en ADO.

Página 10

MIN / MAX. Devuelve el valor mínimo o máximo de una agrupación. Por ejemplo: SELECT orderid, MAX(quantity) FROM [order details] WHERE orderid BETWEEN 11000 AND 11002 GROUP BY orderid COUNT(expression). Hace un recuento de las filas en una consulta. Por ejemplo: SELECT COUNT(*) FROM employees WHERE employeeid = 2 Devuelve el número de filas que el campo employeedid es igual a 2. En este caso sólo hay una fila. Si por el contrario ejecutamos esta orden: SELECT COUNT(*) FROM employees El resultado será nueve. Nos ha dado el número total de filas que contiene la tabla employees. Supongamos que queremos obtener el recuento para una columna concreta. En ese caso, utilizaremos una sentencia como esta: SELECT COUNT(fax) FROM customers En este caso hemos pedido saber cuántas filas hay en la tabla customers, por el campo fax. El resultado es 69. Pero esta consulta podría hacernos pensar que el resultado es idéntico a preguntar por todos, por ejemplo: SELECT COUNT(*) FROM customers Si ejecutamos esta consulta, descubriremos que el resultado es 91. ¿Cómo puede ser?. La respuesta está en los valores null. Si observamos la ventana de mensajes cuando ha realizado la primera consulta, vernos un mensaje con este texto: “Advertencia: valor NULL eliminado por el agregado u otra operación SET.” Significa que cualquier valor null sobre un agregado del tipo, count, avg, etc, las columnas que contienen el null, no son consideradas para el cálculo y por tanto no cuentan. En una media, el valor no está representado sobre todas las filas, sino que se aplica sobre todas las filas que no contienen null. Por ejemplo, si quisiéramos saber cuántas son null, utilizaremos la siguiente sentencia: SELECT COUNT(*) FROM customers WHERE fax IS NULL El resultado de la consulta es 22, que sumadas a las 69 que dio, hacen un total de 91.

Capítulo 4. Programación en ADO.

Página 11

Count se puede usar junto con Group By para, por ejemplo, saber que empleados reportan a que director: SELECT reportsto, COUNT(*) FROM employees GROUP BY reportsto Hemos agrupado los resultados por reportsto. Como count es un agregado, en la cláusula group by no tiene que ir. La cláusula HAVING. La cláusula Having se usa si existe una cláusula GROUP BY en la consulta. Mientras que la cláusula Where se aplica a todas las filas antes de convertirse en grupo, la cláusula Having se aplica al valor agregado de dicho grupo. Por ejemplo: SELECT reportsto AS manager, COUNT(*) employees GROUP BY reportsto AS reports FROM

En este ejemplo, la columna reportsto aparece con el nombre de manager y la columna del count, como reports. El resultado puede ser equivoco, ya que presupone que todo el mundo de la empresa depende de dos directores. Y si queremos que sólo aparezcan los directores que tienen a su cargo más de cuatro personas. Si usamos la cláusula Where no podemos hacer nada porque Where se ejecuta antes de la agregación y, por tanto, no podemos devolver filas basándonos en la agregación. Para resolver este problema tenemos la cláusula Having: SELECT reportsto AS manager, COUNT(*) AS reports FROM employees GROUP BY reportsto HAVING COUNT(*) > 4 Por ejemplo, podemos realizar esta otra consulta que nos devolverá 830 filas. SELECT orderid, SUM(quantity) AS Total FROM [order details] GROUP BY orderid Si quisiéramos filtrar la instrucción para que sólo nos de aquellos cuya cantidad es superior a 300, podríamos usar una cláusula Having como sigue a continuación: SELECT orderid, SUM(quantity) AS Total FROM [order details] GROUP BY orderid HAVING SUM(quantity) > 300 En este caso solo nos da dos filas afectadas.

Capítulo 4. Programación en ADO.

Página 12

Predicados Distinct y ALL Supongamos que deseamos obtener un listado de todos los Id de proveedores de todos los productos que tenemos almacenados. La sentencia podría ser: SELECT supplierid FROM products WHERE unitsinstock > 0 La sentencia nos devuelve toda la lista de productos, repitiendo el id de proveedor cada vez que hay un producto de ese proveedor. La solución, si sólo queremos saber que proveedores tienen productos, pasaría por el uso del predicado Distinct. La sentencia podría quedarnos: SELECT DISTINCT supplierid FROM products WHERE unitsinstock > 0 Ahora sólo muestra un Id de proveedor aunque aparezca en más de una fila. Por defecto siempre el predicado es ALL, lo que significa que incluye todas las filas.

4.2.2. La instrucción INSERT Insert es una instrucción de acción. Le indica a SQL Server lo que vamos a hacer con esta instrucción, ya que todo lo que va a continuación son los detalles de la operación. Sintaxis: INSERT [intro] <tabla> [(lista_de_columnas)] VALUES (valores_de_datos) La palabra clave intro es de relleno. Su único propósito es que la instrucción sea más legible. Es completamente opcional pero su uso es recomendable por el tema de la legibilidad. Después de intro viene la tabla en la que deseamos insertar los valores: La lista de columnas. Podemos prescindir de una lista explícita (son listas donde se especifica concretamente que columnas recibirán los valores). Es opcional, pero si no suministramos ninguna, tenemos que tener mucho cuidado de asegurarnos que los valores corresponden a las columnas. A continuación y después de la palabra VALUES, van los valores de las columnas separados por comas y entre paréntesis. Si queremos no establecer un valor en una columna podemos usar la palabra default para que la base de datos ponga uno predeterminado. También y, siempre que este permitido, podemos usar Null. Para el siguiente ejemplo usamos Pubs. Ejemplo de Insert: INSERT INTO stores VALUES ('Prue', 'Prueba Almacen', 'Prueba 15', „Madrid‟, 'MD', '00319')
Capítulo 4. Programación en ADO. Página 13

La ejecución de esa instrucción genera una nueva fila dentro de la tabla stores. Si intentamos repetir la instrucción, nos dará el siguiente error: “Infracción de la restricción PRIMARY KEY 'UPK_storeid'. No se puede insertar una clave duplicada en el objeto 'dbo.stores'.” El motivo es porque la columna stor_id es un campo de Clave Primaria y no admite repeticiones. A pesar de que ha funcionado bien, lo ideal hubiera sido haber suministrado una lista de columnas explícita. Veamos un ejemplo en que facilitamos una lista de columnas: INSERT INTO stores (stor_id, stor_name, city, state, zip) VALUES ('Pru2', 'Prueba Almacen', 'Madrid', 'MD', '00319') En este segundo ejemplo hemos omitido el campo correspondiente a stor_address, pero no importa porque al ser un campo que permite valores null, dará de alta la fila colocando un null en esa columna. El hecho de usar una lista de columnas nos permite especificar qué valores van a qué columnas. Si comprobamos la inserción descubriremos que en la columna que no hemos dado un valor a colocado null. Una columna que acepta valores null permite que en una instrucción Insert se omitan valores para esa columna. Si una columna no permite null, entonces hay que proporcionar una de estas tres condiciones: La columna se ha definido con un valor predeterminado. Un valor predeterminado es un valor constante que se inserta si no se proporciona ningún valor. La columna está definida para admitir valores generados por el sistema. El valor más común de este tipo es un valor Identity. Suministramos un valor para la columna. Nota: Existe un procedimiento almacenado en el sistema que permite ver las propiedades de cualquier objeto de la base de datos en la ventana de consultas: sp_help. Para poder ejecutarlo usaremos la siguiente sintaxis: EXEC sp_help sales

Instrucción Insert Into - Select La instrucción Insert que hemos desarrollado permite insertar sólo una fila cada vez. Si necesitamos insertar más de una fila, entonces debemos de usar la instrucción Select en la sintaxis de la instrucción Insert.

Capítulo 4. Programación en ADO.

Página 14

Sintaxis: INSERT INTO <nombre_tabla> [<lista_columnas>] <instrucción SELECT> En una instrucción Insert con Select podemos seleccionar los registros a insertar de distintos orígenes: Otra tabla de nuestra base de datos. Una base de datos totalmente diferente en el mismo servidor. Una consulta de otro SQL Server u otros datos. De la misma tabla (normalmente para ordenar cálculos matemáticos). En el siguiente ejemplo creamos una tabla en memoria y le asignamos por mediación de la sentencia Insert con Select datos de la base de datos Northwind, tabla orders: USE northwind DECLARE @mitabla Table ( OrderId int, CustomerId char(5) ) INSERT INTO @mitabla SELECT OrderId, CustomerId northwind.dbo.Orders WHERE OrderId BETWEEN 10240 AND 10250 SELECT * FROM @mitabla Ejemplo: Uso InsertarTablaMemoria

FROM

4.2.3. La instrucción UPDATE La instrucción Update permite modificar valores de registros que se encuentran en la tabla. Una instrucción Update puede crearse a partir de varias tablas pero sólo puede realizar cambios a una. Esto quiere decir que podemos crear una condición, o recuperar valores de cualquier número de tablas distintas, pero sólo una tabla a la vez puede estar sujeta a la acción de la actualización. Sintaxis: UPDATE <nombre de tabla> SET <columna> = <valor> [, <columna> = <valor> ] [FROM <tabla-s de origen>] [WHERE <condición restrictiva>]

Capítulo 4. Programación en ADO.

Página 15

Tomando la base de datos Pubs, supongamos que queremos actualizar el valor de la columna city, de la tabla stores: UPDATE stores SET city = 'Toledo' WHERE stor_id = 'prue' En esta sentencia hemos cambiado el valor de una sola columna, pero podemos cambiar varias columnas a la vez: UPDATE stores SET city = 'Toledo', state = 'CM' WHERE stor_id = 'prue' Pero los cambios pueden afectar a grupos de registros que cumplan una condición. Por ejemplo vamos a probar una sentencia Select en la que aplicamos el operador Like: SELECT title_id, price FROM titles WHERE title_id LIKE 'BU%' Al ejecutarla vemos que muestra todos los títulos que empiezan por „BU‟, sin tener en consideración más valores dentro de la cadena. Ahora, viendo el resultado, vamos a aplicar una sentencia Update usando el operador Like para cambiar el precio en los libros que coincidan en la consulta: UPDATE titles SET price = price * 1.1 WHERE title_id LIKE 'BU%' Este ejemplo actualiza los valores de la columna precio, incrementando su valor en un 10%, para todos aquellos libros cuyo campo title_id empiecen por „BU‟. Si volvemos a realizar un Select para ver los resultados, observaremos que cada elemento tiene un número variable de decimales. Si quisiéramos ajustarlo podemos utilizar una función de redondeo. UPDATE titles SET price = ROUND(price * 1.1, 2) WHERE title_id LIKE 'BU%'

4.2.4. La instrucción DELETE La instrucción Delete puede que sea de las que hemos visto hasta ahora de las más sencillas. No incorpora listas de campos, ya que la posibilidad de borrar es a toda la fila y no a una parte de ella. Sintaxis: DELETE <nombre_tabla> [WHERE <condición_busqueda>] La cláusula Where funciona como hemos venido viendo hasta ahora.

Capítulo 4. Programación en ADO.

Página 16

Ejemplo: DELETE stores WHERE stor_id = 'prue' Puede ocurrir que en una operación Delete aparezca un mensaje como el que aparece a continuación: Instrucción DELETE en conflicto con la restricción REFERENCE "FK__sales__stor_id__0AD2A005". El conflicto ha aparecido en la base de datos "pubs", tabla "dbo.sales", column 'stor_id'. Este error se produce porque la tabla stores, tiene su campo clave y está relacionado con la tabla sales, columna stor_id. No nos deja borrar la fila porque si lo hace dejaría huérfanos registros en otras tablas. Se perdería la integridad referencial. Para poder borrar el registro, es necesario borrar antes los que dependen de él.

4.2.5. La cláusula JOIN En las bases de datos relacionales las tablas grandes se suelen dividir en tablas más pequeñas para evitar la redundancia de datos, ahorrar espacio, mejorar el rendimiento y aumentar la integridad de datos. Cuando una base de datos está normalizada es cuando se han aplicado técnicas de diseño que permiten tener las tablas grandes en tablas más pequeñas. Esto hace que al final los datos se tengan que tomar de más de una tabla. El proceso de combinación de tablas en un conjunto de resultados requiere el uso de la cláusula JOIN, que incluye: INNER JOIN OUTER JOIN (tanto LEFT como RIGHT) FULL JOIN CROSS JOIN

Usamos JOIN para combinar la información de dos o más tablas en un solo conjunto de resultados. El conjunto de resultados puede ser tratado como una tabla virtual. Tiene columnas, tipos de datos y valores. A JOIN hay que indicarle como queremos que se una la información. En el siguiente esquema podemos ver un ejemplo del uso de JOIN

Capítulo 4. Programación en ADO.

Página 17

Tabla Modelos
IDCoche 1 2 3 4 Modelo Coche Xara Picaso Megant C5 Ibiza IDFabricante 1 3 1 2

Campo Clave
El campo IDFabricante Relaciona ambas tablas.

Tabla Fabricantes
IDFabricante 1 2 3 Nombre Fabricante Citroen Seat Renault

Tabla Resultado de aplicar una sentencia JOIN
IDFabricante 1 1 Nombre Fabricante Citroen Citroen IDCoche 1 3 Modelo Coche Xara Picaso C5

Como podemos ver la tabla Fabricantes tiene un campo clave llamado IdFabricante que vincula esta tabla con la tabla Modelos. Al usar una sentencia JOIN en una consulta con ambas tablas, obtenemos una tabla de resultados, en la que se unen los registros que coinciden con el campo, IDFabricante en la tabla modelos, con el registro seleccionado de la tabla fabricante, cuyo IDFabricante sea el mismo. En el ejemplo obtenemos los modelos para Citroen. Sintaxis: SELECT <lista de selección> FROM <primera tabla> <tipo_de-unión> <segunda tabla> [ON <condición_de_unión>]

Capítulo 4. Programación en ADO.

Página 18

INNER JOIN Esta cláusula es la más conocida de JOIN. Empareja los registros basándose en uno o más campos comunes, como la mayoría de cláusulas JOIN, pero INNER JOIN devuelve sólo los registros que se corresponden con el campo (o campos) que hayamos especificado en JOIN. En el siguiente ejemplo usamos la base de datos Northwind: SELECT * FROM Products INNER JOIN Suppliers ON Products.SupplierID = Suppliers.SupplierID Si nos fijamos en el resultado de la consulta, descubriremos que la columna SupplierID (ID Proveedor) aparece dos veces, pero no se sabe qué columna proviene de que tabla. Además, podemos observar que se han devuelto todas las columnas de ambas tablas y, por último, las primeras columnas son de la primera tabla. Aparecen todas las columnas porque en el Select le hemos indicado con el * que ponga todas. Esto no es muy recomendable en una sentencia Join, debido a los problemas de lectura que acarrea. Supongamos que lo que queríamos era el nombre del proveedor, de todos nuestros productos, entonces la consulta podría quedarnos así: Vamos a realizar la misma consulta pero esta vez usando un alias para el nombre de la tabla. Para crear un alias, justo detrás del nombre de la tabla va el alias: SELECT tp.*, companyname FROM Products tp INNER JOIN Suppliers ON tp.SupplierID = Suppliers.SupplierID Si usamos un alias, hay que usar el alias en todas las apariciones del nombre de la tabla en la consulta. Supongamos otro ejemplo: SELECT tp.productid, ts.supplierid, tp.productname, ts.companyname FROM Products tp INNER JOIN Suppliers ts ON tp.SupplierID = ts.SupplierID En este ejemplo, usamos el alías tp para la tabla productos y el alias ts para la tabla suppliers. En el select hemos especificado que columnas y en qué orden queremos que aparezcan en la tabla de respuesta. Un JOIN del tipo INNER JOIN es una unión exclusiva, es decir, excluye todos los registros que no tienen un valor en ambas tablas. Vamos a ver otro ejemplo. Imaginemos que nos piden cuales son los clientes que nos han hecho algún pedido. Para ello necesitamos la tabla
Capítulo 4. Programación en ADO. Página 19

Customers(Clientes) y Orders(Pedidos). El campo CustomerID de la tabla Customers, es la relación existente entre ambas tablas. Para que un pedido sea válido necesitamos dar de alta el ID del cliente en el pedido. Por tanto, la sentencia podría ser: SELECT DISTINCT tc.customerid, tc.companyname FROM Customers tc INNER JOIN Orders tod ON tc.customerid = tod.customerid Hemos usado la palabra clave DISTINCT porque sólo queremos conocer que clientes han hecho algún pedido, no todos los pedidos. Probemos un ejemplo que combina dos JOIN sobre la base de datos Pubs, utilizando tres tablas. Buscamos una consulta que devuelva todos los autores que han escrito libros y los títulos de dichos libros. Lo primeo que podemos observar es que en la tabla authors, tenemos el autor y en la tabla titles, los títulos. En principio no tenemos nada que los una, pero hay una tabla llamada titleauthor, que es una tabla que resuelve las relaciones de varios a varios. También se las conoce como tablas unión o asociadas. SELECT ta.au_lname + ', ' + ta.au_fname AS "Autor", tl.title FROM authors ta INNER JOIN titleauthor tt ON ta.au_id = tt.au_id INNER JOIN titles tl ON tl.title_id = tt.title_id El operador + utilizado en el Select, permite ver el contenido del campo Lname y Fname en uno solo. La coma que aparece entre los dos es por un tema visual, pudiéndose colocar cualquier símbolo o carácter. Como en cualquier lenguaje que maneje string, el operador + concatena los string en uno sólo. Esta concatenación deja en una columna a la que llamamos Autor, el contenido de los dos campos separados por una coma. OUTER JOIN Este tipo de cláusula es más bien una excepción, no una regla y no porque no tenga su uso, sino por lo siguiente: Normalmente deseamos la exclusividad proporcionada por INNER-JOIN. A veces es un desconocimiento por parte de los programadores. Normalmente existen otros métodos para conseguir lo mismo. Una cláusula INNER JOIN es exclusiva, mientras que OUTER JOIN y FULL JOIN son inclusivas. Pueden resultar muy útiles desde el punto de vista del rendimiento cuando se utilizan en lugar de subconsultas anidadas. JOIN tiene lados (uno izquierdo y otro derecho). La primera tabla con nombre se considera que está a la izquierda y la segunda que está a la

Capítulo 4. Programación en ADO.

Página 20

derecha. INNER JOIN trata a los dos lados de la misma forma. En las cláusulas OUTER JOIN, es muy importante saber cuál es nuestro izquierdo y derecho. El tema es que la tabla que aparece antes del OUTER JOIN es la izquierda y la que aparece a continuación es la derecha. Esto es muy importante cuando especificamos con Left o Right. Si decimos LEFT OUTER JOIN le estamos diciendo agrega toda la información de la tabla que está a la izquierda y, con RIGHT OUTER JOIN, le estamos diciendo que tome toda la información de la tabla que está a la derecha. Supongamos que necesitamos saber cuáles son nuestros descuentos, la cantidad total de cada uno y las tiendas que los utilizan. Si examinamos nuestra base de datos pubs, podremos ver que tenemos la siguiente tabla sobre descuentos (discounts) y tiendas (stores). Podemos unir estas dos tablas basándonos en el ID de tienda, stor_id. Si lo hiciésemos con INNER JOIN, la consulta resultaría: SELECT discounttype, discount, ts.stor_name FROM discounts td JOIN stores ts ON td.stor_id = ts.stor_id El resultado sería de sólo una fila. Buscamos los descuentos que tenemos, los que existen actualmente. Pero esta consulta sólo nos proporciona los descuentos para los que existen tiendas correspondientes. Buscamos todos los descuentos y las tiendas donde se aplican: SELECT discounttype, discount, ts.stor_name FROM discounts td LEFT OUTER JOIN stores ts ON td.stor_id = ts.stor_id Podemos comprobar que nos ha devuelto todas las filas de discounts. Cuando se ejecuta, en la tabla de la derecha si una fila de la tabla discounts no tiene una coincidencia con la tabla stores, muestra un null. En nuestro caso de los tres registros que salen, dos son null y el tercero es el nombre de una tienda. ¿Qué ocurre si cambiamos el LEFT por RIGHT: SELECT discounttype, discount, ts.stor_name FROM discounts td RIGHT OUTER JOIN stores ts ON td.stor_id = ts.stor_id Aunque parece un cambio pequeño cambia totalmente la salida de la consulta. Una de las ventajas que nos puede ofrecer OUTER JOIN es la posibilidad de encontrar registros huérfanos o no coincidentes. Siguiendo con el ejemplo anterior, veamos como lo podemos aplicar para buscar tiendas que no tengan asignados ningún tipo de descuento.

Capítulo 4. Programación en ADO.

Página 21

Una cláusula OUTER JOIN nos devuelve un valor null en las columnas basadas en los descuentos siempre que no exista una correspondencia. Ejemplo: SELECT ts.stor_name AS 'Nombre de la Tienda' FROM discounts td RIGHT OUTER JOIN stores ts ON td.stor_id = ts.stor_id WHERE td.stor_id IS NULL El resultado de esta consulta son cinco filas afectadas donde la tienda no tiene asignado ningún tipo de descuento. Si analizamos la consulta comprobamos lo siguiente: Si la columna stores.stor_id tiene un valor distinto de NULL, entonces, según el operador ON de la cláusula JOIN, si existen registros en discounts, entonces discounts.stor_id también tiene que tener el mismo valor que stores.stor_id (ON td.stor_id = s.stor_id) Si la columna stores.stor_id tiene un valor distinto de NULL, entonces, según el operador ON de la cláusula JOIN, si no existe ningún registro en discounts, entonces discounts.stor_id se devolverá como NULL. Si stores.stor_id tiene un valor NULL y discounts.stor_id también tiene un valor NULL, no existiría ninguna unión y discounts.stor_id devolverá NULL porque no existe ningún valor coincidente. Un valor NULL nunca es igual a otro valor NULL. Nota: A partir de la versión SQL server 7.0, un valor null nunca es igual a otro valor null. Pero puede ocurrir que mantenga el servidor de SQL compatibilidad con la versión 6.5, teniendo desactivados los ANSI_NULLS a través de las opciones de servidor o de una instrucción SET. El tener desactivado los ANSI_NULLS es una infracción al estándar ANSI y ya no es compatible con la configuración actual de SQL Server. FULL JOIN Esta cláusula también es conocida como FULL OUTER JOIN, hace corresponder los datos de ambos lados de JOIN, independientemente del lado JOIN que sea. Aunque al principio parece una muy buena elección, luego casi nunca se usa. Con FULL JOIN podremos obtener todos los registros coincidentes basándonos en el campo (o campos) de JOIN. También podremos obtener cualquier registro existente en la parte de la izquierda, devolviéndose todas las columnas con valores NULL de la parte derecha. Pero también podemos obtener cualquier registro existente sólo en la parte derecha, devolviendo los valores NULL de las columnas de la parte izquierda. Usamos la base de datos Pubs. Ejemplo:
Capítulo 4. Programación en ADO. Página 22

SELECT ta.au_fname, ta.au_lname, tp.pub_name FROM authors ta FULL OUTER JOIN publishers tp ON ta.city = tp.city ORDER BY tp.pub_name En este ejemplo podemos ver que ha obtenido los registros coincidentes de ambos lados, colocando NULL cuando en la otra tabla no había valores. CROSS JOIN Es una cláusula bastante extraña en su funcionamiento y su uso bastante restringido. Se sabe que para aplicaciones matemáticas, se suele aplicar, ya que lo que hace en realidad es generar tablas cartesianas. No dispone del operador ON, a diferencia de las otras instrucciones JOIN. Vemos un ejemplo de su aplicación: SELECT a.au_fname, a.au_lname, p.pub_name FROM authors a CROSS JOIN publishers p Como podemos ver en los resultados, obtiene todas las filas de authors, a las que le añade el equivalente de publishers, y todas las filas de publishers a las que le añade el equivalente de authors. Es decir, multiplicando el número de registros de una tabla por otra, sabremos cuantos registros obtendrá la consulta. 4.2.6. Create Create se aplica tanto a bases de datos como a tablas. El comando Create soporta dos variantes en función de si creamos una Base de Datos, o creamos una tabla dentro de la Base de Datos. Crear la Base de Datos: CREATE DATABASE <nombrebasedatos> Crear la tabla: CREATE TABLE [nombrebasedatos] nombretabla(<nombrecolumna> <tipodatos>) Estructura de los tipos de datos:
Tipo SQL Integer Real Float Char Varchar Binary DATE Tipo MS Access Número entero largo Número simple Número doble Texto Texto Binario Fecha/Hora Tipo SQL Framework SqlInt32 SqlSingle SqlDouble SqlString SqlString SqlBinary SqlDateTime de .NET

Capítulo 4. Programación en ADO.

Página 23

La diferencia entre el tipo char y Varchar, radica en que el primero (longitud fija) rellena con espacios en blanco la longitud de texto que no se haya usado y el segundo no (longitud variable). Es decir, al final el char debe obligatoriamente grabar un número concreto de bytes definidos por su tamaño, mientras que Varchar, podrá almacenar cualquier valor de bytes que no exceda su longitud máxima. Además de Varchar, existe en SQL el tipo NVarchar. La diferencia entre ambos radica en que cualquier tipo de datos de SQl que empiece por N, utiliza la codificación Unicode que implica que para guardar un carácter utiliza dos bytes y por tanto, usa un juego de caracteres mucho mayor. Estos últimos no se suelen usar a no ser que se use por problemas de compatibilidad con aplicaciones o, por que se usa en idiomas como el japonés. Cuando se crean columnas en la base de datos, es posible especificar si una columna concreta es la clave principal de la tabla (PRIMARY KEY). Esto significa que una columna del tipo Primary Key, no puede tener ni valores del tipo null, ni valores duplicados (unique). Este campo será el punto de acceso de la base de datos a los datos de la tabla. También podemos hacer que un campo, sin ser Primary Key, contenga datos que no sean duplicados. Para ello, usaremos la cláusula UNIQUE. Esto hace que sea el propio sistema de bases de datos, el que verifique si existe el dato en la tabla y no sea necesario realizar por programa una comprobación. La cláusula NOT NULL permite que no se pueda dejar un campo sin información, es decir, que el campo contenga el valor de tipo null (sin datos). Ejemplo de creación de una tabla: CREATE TABLE Empleados(Nombre VARCHAR(30) NOT NULL, Apellidos VARCHAR(50) NOT NULL, Direccion VARCHAR(100) NOT NULL, Departamento VARCHAR(30) NOT NULL)

4.2.7. La instrucción DROP Esta instrucción permite borrar una base de datos o una tabla. Su sintaxis es la siguiente: Borrar la Base de Datos: DROP DATABASE <nombre de la base de datos> Crear la tabla: DROP TABLE <nombre de la tabla>

Capítulo 4. Programación en ADO.

Página 24

4.3. El objeto DataTable
El objeto DataTable, al igual que el objeto DataSet que veremos más adelante, existen desde las primeras versiones de Net Framework. Son tan importantes para el conjunto de datos, que han ido aumentando en prestaciones e importancia con las distintas versiones. El DataSet ha sido siempre el núcleo de ADO .Net, proporcionando una representación en memoria interna de datos relacionales, incluidos claves, restricciones e incluso capacidad de consulta. Muchas veces se trabaja con una sola tabla, y el uso del DataSet era casi obligado, dado que el objeto DataTable fuera de él no tenía muy buena funcionalidad. A partir de la versión de Net Framework 2.0, se mejoró notablemente la clase DataTable, convirtiéndola en un elemento muy importante y con una funcionalidad propia que hace que ya no sea dependiente del objeto DataSet. Un DataTable es en realidad un representación de una tabla de datos, donde encontramos otros dos objetos: el DataColumn, que permite especificar y manejar las columnas; y el objeto DataRow, que permite acceder a las filas de la tabla. Además podemos aplicar restricciones usando el objeto Constrains, así como relaciones entre tablas usando el objeto DataRelations. ADO, por mediación del DataTable y el DataSet, proveen de todos los elementos necesarios para desarrollar una base de datos sin necesidad de contar con un proveedor específico. El DataTable, incluye la colección DataRowCollection de objetos DataRow (filas de la tabla), la colección DataColumnCollectiom de objetos DataColumn (columna de datos) y la colección ConstraintCollection de objetos Constraint (restricciones).

4.3.1. Cargar valores en una tabla. El objeto DataColumn y DataRow 4.3.1.1. El objeto DataColumn. Para crear una tabla, es necesario empezar por definir las columnas que contendrán la tabla. Una columna tiene dos valores importantes a definir: el nombre de la columna y el tipo de dato que contiene. Podemos declarar la columna de dos formas: Podemos añadir columnas a la colección de columnas del objeto DataTable, especificando el nombre de la columna y el tipo de datos que guarda:
Capítulo 4. Programación en ADO. Página 25

TablaDatos.Columns.Add("Producto", typeof(string));

Una variante de esta forma podría ser esta:
Type Cadena = typeof(string); TablaDatos.Columns.Add("Producto", Cadena);

En ambos casos declaramos la columna con su nombre y su tipo. Pero si lo que queremos es utilizar más funcionalidades de las columnas, como son los valores autoincrementados, campos clave y demás, la forma sería:
DataColumn ColumnaIdentidad = new DataColumn(); ColumnaIdentidad.AutoIncrement = true; ColumnaIdentidad.AutoIncrementSeed = 1; ColumnaIdentidad.AutoIncrementStep = 1; ColumnaIdentidad.DataType = typeof(int); ColumnaIdentidad.Unique = true;

En este ejemplo, hemos creado una columna que será una columna del tipo autoincrementado, cuyo valor inicial es 1 (AutoIncrementSeed) y los incrementos serán de uno en uno (AutoIncrementStep). La propiedad Unique a true indica que la columna es de valores únicos, lo que significa que no puede contener valores repetidos. Una vez declarada la columna, la forma de agregarla a la colección de columnas del objeto DataTable sería:
TablaDatos.Columns.Add(ColumnaIdentidad);

Cuando cargamos las distintas columnas en el objeto DataTable, lo que tenemos, es la estructura de la tabla. Pero ahora es el momento de empezar a dar valores utilizando para ello el objeto DataRow. 4.3.1.2. El objeto DataRow. Este objeto tiene la finalidad de crear una fila que cumpla con la estructura de la tabla, para posteriormente, dar de alta los valores que contendrá la tabla. Podemos crear una fila, especificando manualmente las columnas que contendrá, pero dado que usaremos el objeto DataRow para dar de alta una fila sobre un objeto DataTable ya creado, lo razonable sería crear una fila nueva a partir del objeto DataTable:
DataRow FilaInsertar = TablaDatos.NewRow();

En este ejemplo, hemos creado una fila con la estructura que tiene el objeto DataTable, TablaDatos. Suponiendo que la tabla sea una tabla que contenga las siguientes columnas: Nombre, Apellidos y DNI, la forma de dar valores sería:
Capítulo 4. Programación en ADO. Página 26

FilaInsertar[0] = "Alvaro"; FilaInsertar[1] = "Sánchez Sánchez"; FilaInsertar[2] = “5746565-K”;

En este ejemplo, cómo podemos observar, utilizamos el nombre del objeto DataRow facilitando entre corchetes el número de columna al que deseamos dar valores. Se comporta como una matriz o colección y el índice comienza en cero. También es posible dar el nombre de la columna en lugar del índice. Por último, para añadir los valores al DataTable:
TablaDatos.Rows.Add(FilaInsertar);

DataRow, incluye la propiedad RowState, que permite saber si la fila cambió y cúal fue el motivo del cambio, pudiendo conocer cualquier de estos estados: Added (añadir), Deleted (borrar), Modified (modificar) y Unchanged (sin cambios). Existe además el DataView, que representa una vista personalizada que puede enlazar datos de un DataTable para ordenación, filtrado, búsqueda, edición y exploración.

4.3.2. Recorrer los valores de un DataTable Las filas de un objeto DataTable son una colección y por tanto podemos recorrerla con un bucle for o con un bucle foreach:
for (int i = 0; i < TablaDatos.Rows.Count; i++) { DataRow Fila = TablaDatos.Rows[i]; richTextBox1.Text += Fila[1]; }

En este ejemplo, obtenemos del objeto DataRow, Fila, el valor de la columna 1 (Fila[1]). Si en lugar de usar un bucle for usamos un bucle foreach:
foreach (DataRow Fila in TablaDatos.Rows) richTextBox1.Text += Fila[1];

Como podemos observar, es más cómodo utilizar un bucle foreach que un bucle for. 4.3.3. Recorrer los valores de un DataTable usando un DataReader. Por mediación del objeto DataTableReader, podemos implementar un DataReader, con un funcionamiento similar a un ADO Conectado (este punto se verá más adelante en este mismo capítulo).
Capítulo 4. Programación en ADO. Página 27

DataTableReader Lector = TablaDatos.CreateDataReader(); while (Lector.Read()) { MessageBox.Show(Lector[2].ToString()); }

En este ejemplo hemos implementado un objeto DataTableReader que nos permite recorrer el DataTable como si fuera una consulta realizada en modo ADO Conectado. Las filas del DataTable son volcadas a Lector y, al igual que si fuera un objeto DataRow, podemos sacar el valor de la columna, bien sea por su índice o por su nombre. Las columnas del lector son de tipo object. Ver Uso DataTable-01

4.3.4. Cargar un fichero XML en un DataTable. El XML se ha convertido en un estándar para el intercambio de información entre proveedores de datos. En .Net disponemos de la posibilidad de cargar un DataTable desde un fichero en XML. A partir de la versión de Net Framework 2.0, se mejoró notablemente la clase DataTable, convirtiéndola en un elemento muy importante y con una funcionalidad propia que hace que sea muchas veces útil sin necesidad de contar con un objeto DataSet. Dentro de las nuevas funcionalidades que se dieron al objeto DataTable, está la posibilidad de leer y cargar un fichero XML en el objeto. Implementa los métodos ReadXml y WriteXml, similar al uso que tienen en el DataSet. El método ReadXml permite leer un fichero XML a un objeto DataTable:
DataTable objDataTable = new DataTable("MisClientes"); objDataTable.ReadXml("Clientes.xml");

En este ejemplo, cuando creamos el DataTable, le damos un nombre a la tabla, en este caso: MisClientes. A continuación, usando el método ReadXml, cargamos el fichero clientes.xml dentro del DataTable. Automáticamente, se generan las columnas y las filas dentro del mismo.

4.3.5. Grabar un DataTable en un fichero XML. Otra de las posibilidades es escribir el contenido de un DataTable dentro de un fichero XML, lo que nos permite un fácil intercambio de información con los actuales sistemas de bases de datos y aplicaciones. Para poder realizar una escritura del DataTable en el fichero XML, utilizamos el método WriteXML.
Capítulo 4. Programación en ADO. Página 28

DataTable objDataTable = new DataTable(); objDataTable.Columns.Add("IDCliente", typeof(int)); objDataTable.Columns.Add("NombreCliente", typeof(string)); objDataTable.Columns.Add("ApellidosCliente", typeof(string)); objDataTable.Rows.Add(new object[] { 1, "Pedro", "López Sánchez" }); objDataTable.Rows.Add(new object[] { 2, "María José", "Martínez" }); objDataTable.Rows.Add(new object[] { 3, "Jesus", "Sánchez Alpino" }); objDataTable.Rows.Add(new object[] { 4, "Ana", "Moreno López" }); objDataTable.WriteXml("clientes.xml", XmlWriteMode.WriteSchema);

En este ejemplo, usamos el método WriteXml de los DataTable para grabar el fichero XML con los datos contenidos dentro del DataTable, objDataTable. A pesar de que en la documentación de MSDN indica que la inferencia de esquemas es posible en un DataTable desde datos XML, el hecho de inferir esquemas en tiempo de ejecución desde XML provoca una excepción que indica que no es compatible con la clase DataTable. Para evitar esto, en el ejemplo Uso DataTableXML, se usa la opción XmlWriteMode.WriteSchema para incluir el esquema en la parte superior del documento. Ejemplo Uso DataTableXML

Capítulo 4. Programación en ADO.

Página 29

4.4. El objeto DataSet
El DataSet representa una cache de memoria con datos y es el eje de la arquitectura ADO.NET. Cada DataSet puede contener varios objetos DataTable, y cada DataTable contiene datos de un solo origen, como SQL Server o Access. Igualmente y, como hemos visto anteriormente, podemos crear un DataTable con datos locales sin necesidad de conectar a ningún origen de datos en SQL. Este DataTable, evidentemente, también puede ser cargado dentro del DataSet. Es decir, el DataSet puede mantener múltiples datos que pertenecen a distintos orígenes de datos. El DataSet, incluye la colección DataTableCollection de objetos DataTable (que son las tablas de datos) y la colección DataRelationCollection de objetos DataRelation (relaciones entre las tablas). Para conectar datos a un DataSet podemos usar un objeto DataAdapter (se estudiará más adelante), que almacena los datos leídos del origen de datos en un conjunto de datos. Cuando deseamos modificar ese origen de datos desde nuestra aplicación, volverá a realizar la conexión con el origen de datos, suministrándole la nueva información desde el conjunto de datos que se encuentra en nuestra máquina. Podríamos acceder directamente al origen de datos por mediación de comandos de SQL o procedimientos almacenados. 4.4.1. Cargar objetos DataTable en el DataSet El elemento básico de información dentro de un DataSet es el DataTable. Por tanto, no tiene sentido crear un DataSet si luego no vamos a cargar en su interior objetos DataTable. Para cargar un DataTable dentro del DataSet disponemos de la colección Tables del DataSet. Por ejemplo:
//Damos nombres a las tablas. objDataTable1.TableName = "Empleados"; objDataTable2.TableName = "Categorias"; //Agregamos las tablas al DataSet. objDataSet.Tables.Add(objDataTable1); objDataSet.Tables.Add(objDataTable2);

En este ejemplo, añadimos a la colección Tables del DataSet el objeto objDataTable1 y el objetoDataTable2, ambos del tipo DataTable. Previamente, usando la propiedad TableName, hemos asignado un nombre a cada Tabla. 4.4.2. Relacionar objetos DataTable Una función muy importante de los DataSet es la de manejar los DataTable como si de una Base de Datos Relacional se tratara. Esto nos permite crear DataTables dentro del DataSet, para después vincularlos entre sí
Capítulo 4. Programación en ADO. Página 30

y aumentar las prestaciones de los datos. En la siguiente figura podemos ver el diseñador de DataSet con varias tablas y las relaciones entre ellas.

Diseñador de DataSet
Relaciones entre DataTables

Nombre del DataTable

Columnas del DataTable. Objeto DataColumn

Objetos DataTable

Figura: Imagen del Diseñador de DataSet.

Ver Uso DataSet-ADO. Si usamos el diseñador de DataSet definiremos las relaciones con las herramientas visuales que para ello tiene definido el objeto. Pero en el caso de que necesitemos desarrollar todo el trabajo desde el código, los pasos a seguir son: Definir las columnas que actuaran como campos clave dentro del DataTable, especificando incluso las características de unicidad, campo clave y demás características que usaríamos en una tabla dentro de un gestor de bases de datos relacional. Ejemplo:
//Creamos una columna que será columna clave del DataTable. //Será una columna de autoincremento y valores únicos. DataColumn IdEmpleado = new DataColumn("IdEmpleado", typeof(int)); IdEmpleado.AutoIncrement = true; IdEmpleado.AutoIncrementSeed = 1; IdEmpleado.AutoIncrementStep = 1; IdEmpleado.Unique = true; //Agregamos la columna a la colección de columnas del DataTable objDataTable1.Columns.Add(IdEmpleado); //Creamos una matriz de columnas con la columna o columnas que serán //Columna clave en la tabla. DataColumn[] Claves = { IdEmpleado }; //Especificamos que columna o columnas son la clave del DataTable. objDataTable1.PrimaryKey = Claves;

Capítulo 4. Programación en ADO.

Página 31

A continuación creamos las relaciones que pudieran existir entre las distintas tablas dentro del DataSet, tal y como podemos ver en este ejemplo:
//Existe una columna que es la clave principal y otra que es la clave //foránea. //Definimos dos objetos DataColumn con los nombres de las dos columnas //que vamos a relacionar. DataColumn ClavePrincipal = objDataSet.Tables["Categorias"].Columns["IdCategoria"]; DataColumn ClaveForanea = objDataSet.Tables["Empleados"].Columns["IdCategoria"]; //A Continuación aplicamos la relación. DataRelation relacionClaves = new DataRelation("RelacionCategoria", ClavePrincipal, ClaveForanea); objDataSet.Tables["Empleados"].ParentRelations.Add(relacionClaves);

4.4.3. Obtener objetos DataTable. Para obtener un DataTable que se encuentra dentro de un DataSet, accedemos por mediación de la colección Tables, igual que cuando guardamos el DataTable. Ejemplo:
DataTable objTabla = objDataSet.Tables["Empleados"];

En este ejemplo hemos accedido al DataTable Empleados y colocado una copia en objTabla. También podemos acceder por el índice del DataTable dentro de la colección de Tables del DataSet. Por ejemplo, si sabemos que el DataTable Empleados es el cero dentro del índice, podemos recuperarlo de la siguiente forma:
DataTable objTabla = objDataSet.Tables[0];

4.4.3.1. El método Select de los DataTable. Podemos obtener datos de un objeto DataTable usando el método Select. En este caso, la información es volcada sobre un objeto del tipo matriz de DataRow. El método Select se comporta de una forma muy similar a la instrucción Select del T-SQL. Un ejemplo de aplicación del método Select sería:
DataRow[] Filas = objDataSet.Tables["Empleados"].Select("IdCategoria = 2");

En este ejemplo obtenemos todas las filas del DataTable Empleados cuyo valor de IdCategoria sea 2. Las filas coincidentes son guardadas en la matriz del tipo DataRow Filas.
Capítulo 4. Programación en ADO. Página 32

Otro ejemplo sería:
DataRow[] Filas = objDataSet.Tables["Empleados"].Select("IdCategoria = 2 AND NombreEmpleado = 'Miguel Angel'");

En este otro ejemplo, la búsqueda se realizar por el IdCategoria igual a 2 y, por el NombreEmpleado igual a Miguel Angel. Si deseamos obtener los datos ordenados, podemos incluir el parámetro Sort del método Select:
DataRow[] Filas = objDataSet.Tables["Empleados"].Select("IdCategoria = 2", "NombreEmpleado DESC");

Este ejemplo ordena los resultados por la columna NombreEmpleado y de formas descendente (DESC). Por defecto es ascendente. Otra posibilidad de ordenación sería:
DataRow[] Filas = objDataSet.Tables["Empleados"].Select("IdCategoria = 2", "ApellidosEmpleado ASC, NombreEmpleado ASC");

En este otro caso el resultado es ordenado primeramente por la columna ApellidosEmpleado de forma ascendente y por la columna NombreEmpleado de forma también ascendente. Otra posibilidad es obtener filas que cumplan un criterio basado en si han sido borradas, modificadas u obtener sus valores originales. Para este punto disponemos del miembro enumerable DataViewRowState:
DataRow[] Filas = objDataSet.Tables["Empleados"].Select(null, null, DataViewRowState.Added);

En este ejemplo obtiene todas las filas que fueron añadidas al DataTable. Las distintas posibilidades del DataViewRowState son: Valor de DataViewRowState Added CurrentRows Deleted ModifiedCurrent ModifiedOriginal OriginalRows Unchanged None Significado Fila nueva Las filas actuales, incluyendo las agregadas, modificadas y sin modificar. Fila eliminada La versión actual, que es una modificación de los datos actuales. La versión original de todas las filas modificadas. Las filas originales, incluyendo las filas que fueron eliminadas. Fila sin modificar. Ninguno

Capítulo 4. Programación en ADO.

Página 33

Parámetros y expresiones válidas en el método Select de los DataTable:
Para utilizar una columna denominada "Column#" en una expresión, se deberá escribir "[Column#]". Ejemplo: Total * [Column#] Puesto que los corchetes son caracteres especiales, se debe utilizar una barra diagonal ("\") para crear un carácter de escape para el corchete, si forma parte de un nombre de columna. Por ejemplo, una columna denominada "Column[ ]" se escribirá: Total * [Column[\]] Sólo se debe crear un carácter de escape para el segundo corchete. Valores Definidos por el Usuario Los valores definidos por el usuario se pueden utilizar en expresiones para compararlos con valores de columnas. Los valores de cadena se deben escribir entre comillas sencillas. Los valores de fecha se deben poner entre signos de libra esterlina (#) o comillas simples (') dependiendo del proveedor de datos. Se permiten decimales y notaciones científicas para los valores numéricos. Por ejemplo: "FirstName = 'John'" "Precio <= 50.00" "Fecha < #1/31/2010#" “Fecha < „31/1/2010‟ Para el caso de las fechas, puede ocurrir que se den errores derivados del uso de los formatos de fecha en americano o en español. Si usamos notación americana usaremos las #, pero para el caso de las fechas en español usamos las comillas simples. Para las columnas que contienen valores de enumeración, el valor se convierte en un tipo de datos entero. Por ejemplo: "EnumColumn = 5" Operadores Se permite la concatenación mediante operadores booleanos AND, OR y NOT. Se pueden utilizar paréntesis para agrupar cláusulas y forzar una precedencia. El operador AND tiene precedencia sobre otros operadores. Por ejemplo: (LastName = 'Smith' OR LastName = 'Jones') AND FirstName = 'John'

Capítulo 4. Programación en ADO.

Página 34

Al crear expresiones de comparación, se permiten los siguientes operadores:

También se admiten los siguientes operadores aritméticos en las expresiones:

Operadores de Cadena Para concatenar una cadena se utiliza el carácter +. El valor de la propiedad CaseSensitive de la clase DataSet determina si en las comparaciones de cadenas se distingue entre mayúsculas y minúsculas. Sin embargo, se puede reemplazar ese valor por la propiedad CaseSensitive de la clase DataTable. Caracteres Comodín Tanto * como % se pueden utilizar indistintamente como caracteres comodín en una comparación LIKE. Si la cadena de una cláusula LIKE contiene un carácter * o %, dichos caracteres se deben establecer como caracteres de escape entre corchetes ([]). Si hay un corchete en la cláusula, los caracteres de corchete se deben establecer como caracteres de escape entre corchetes (por ejemplo, [[] o []]). Se permite un carácter comodín al comienzo y al final de un modelo, al final de un modelo o bien al comienzo de un modelo. Por ejemplo: "ItemName LIKE '*product*'" "ItemName LIKE '*product'" "ItemName LIKE 'product*'" No se permiten los caracteres comodín en mitad de una cadena. Por ejemplo, no se admite 'te*xt'.
Capítulo 4. Programación en ADO. Página 35

Referencia a Relaciones primarias y secundarias Se puede hacer una referencia a una tabla primaria en una expresión anteponiendo Parent al nombre de la columna. Por ejemplo, Parent.Precio hace referencia a la columna denominada Precio de la tabla primaria. Se puede hacer referencia a una columna de una tabla secundaria en una expresión anteponiendo Child al nombre de la columna. Sin embargo, dado que las relaciones secundarias pueden devolver varias filas, se debe incluir la referencia a la columna secundaria en una función de agregado. Por ejemplo, Sum(Child.Precio) devolvería la suma de la columna denominada Precio de la tabla secundaria. Si una tabla tiene varias tablas secundarias, la sintaxis es: Child(RelationName). Por ejemplo, si una tabla tiene dos tablas secundarias denominadas Customers y Orders, y el objeto DataRelation se denomina Customers2Orders, la referencia sería la siguiente: Avg(Child(Customers2Orders).Quantity) Agregados Se admiten los siguientes tipos de agregados:

Normalmente los agregados se llevan a cabo en las relaciones. Se crea una expresión de agregado mediante una de las funciones enumeradas anteriormente y una columna de una tabla secundaria, como se ha descrito en Referencia a relaciones primarias y secundarias. Por ejemplo: Avg(Child.Precio) Avg(Child(Orders2Details).Precio) Un agregado también se puede realizar en una sola tabla. Por ejemplo, para crear un resumen de cifras de una columna denominada "Price": Sum(Precio) Ver: Uso DataTable-DataSet

Capítulo 4. Programación en ADO.

Página 36

4.5. Introducción ADO.NET (ActiveX Data Object)
ADO.NET es una tecnología de Mircrosoft que sustituyo a DAO (Data Access Object), a RDO (Remote Data Object) y a la versión anterior, llamada simplemente ADO. La filosofía de diseño de ADO .NET es evitar el mantener activa permanentemente una conexión con la base de datos, aunque también pude trabajar en modo permanentemente conectado. Esto significa que la aplicación se conecta sólo cuando necesita realizar una tarea con la base de datos, estando el resto del tiempo desconectado del Servidor de Bases de Datos. Al final, esto repercute muy beneficiosamente en el uso del Servidor, ya que permite disponer de un mayor número de conexiones posibles. ADO .NET trabaja como intermediario entre nuestra aplicación y la base de datos. El programa nunca ve directamente los datos, sino que le son suministrados por ADO .NET y, este a su vez, devuelve la información a la base de datos cuando el programa así lo requiere. Todo acceso a la base de datos se realiza desde ADO .NET por mediación de órdenes, que son objetos que encapsulan sentencias de SQL o procedimientos almacenados. Cuando solicitamos datos, estos son almacenados en memoria cache, lo que permite trabajar sobre los datos sin tener una conexión abierta. En realidad trabajamos sobre una copia de los datos originales. Una vez que hemos realizado operaciones con esos datos, el modelo de ADO .NET permite restablecer la conexión con la base de datos y actualizar los datos que estábamos manejando. El formato de transferencia de ADO .NET es el XML. Un fichero XML es un fichero de texto que puede ser enviado con cualquier protocolo, como por ejemplo, HTTP. Esquema del modelo de trabajo de ADO

Capa de Presentación
Aplicación Windows Aplicación Consola Aplicación WEB

Capa Lógica Negocio
Conjunto de Datos

Capa de Datos
Bases de Datos

Adaptador de Datos Adaptador de Datos

Conexión

Conexión

Capítulo 4. Programación en ADO.

Página 37

Esquema de los modos de trabajo de ADO
Aplicación

DataSet
Proveedor de Acceso a Datos

DataReader

Command

DataAdapter

Connection

Leyenda: Modo Conectado Modo Desconectado

Base de Datos

Componentes de ADO .NET. Espacio de nombres: System.Data Los objetos de ADO .NET pertenecen al espacio de nombres System.Data, es decir, proporciona acceso a las clases que permiten trabajar con ADO.NET. Algunos de los componentes que incorpora son: Connection. Responsable de realizar las conexiones con la base de datos. Command. Responsable de las órdenes o comandos sobre la base de datos. DataReader. Lector de los datos. Lee una secuencia de datos de sólo avance y sólo lectura desde un origen de datos. DataAdapter. Representa un conjunto de comandos SQL y una conexión de base de datos que se utilizan para rellenar el objeto DataSet y actualizar el origen de datos.

Capítulo 4. Programación en ADO.

Página 38

Todos estos objetos se analizarán más adelante cuando tratemos los dos modos de acceso de ADO: Conectado y Desconectado. El Proveedor de datos El proveedor de datos es el responsable de crear una comunicación entre la aplicación y el origen de datos, así como de suministrar todos los componentes necesarios para trabajar con los datos. Se utiliza en ambos sentidos, es decir, para recuperar datos de un origen, o para actualizar esos datos en el origen. Cada Servidor de Datos usa su propio proveedor de datos, también llamado proveedor de datos nativo. En algunas circunstancias, podemos encontrarnos que no existe un proveedor nativo y por tanto debemos de usar uno genérico. Esquema del Proveedor de Datos

El Proveedor de Datos
Connection Transaction DataAdapter SelectCommand InsertCommand UpdateCommand DeleteCommand DataReader

Command Parameters

Cada proveedor tiene su correspondiente espacio de nombres asignado. Así encontramos para: SQL Server. El System.Data.SqlClient. Cualquier versión del Sql Server 7, o superior. Oracle. El System.Data.OracleClient ODBC. El System.Data.Odbc OLE DB. El System.Data.OleDb

Por mediación de ODBC y OLE DB, podemos acceder a la mayoría de los Servidores de SQL que hay en el mercado. Para el caso de MySQL, podemos usar ODBC, pero es necesario acudir al fabricante para que nos
Capítulo 4. Programación en ADO. Página 39

suministre el proveedor de datos nativo, ya que estos se comportan mucho mejor que un proveedor genérico como es ODBC. De todas formas, Microsoft facilita en el siguiente enlace proveedores de datos por terceros: http://msdn.microsoft.com/en-us/data/dd363565.aspx Dentro del proveedor de datos, disponemos del objeto Connection, que permite realizar la conexión. En función del proveedor de datos que estemos usando cambia el objeto Connection. Así, si queremos conectar con SQL Server usamos el objeto SqlConnection, para conectarnos a Access, usamos OleDbConnection. El objeto Connection permite, por mediación de propiedades, especificar el id de usuario, el password de conexión, la base de datos que se conecta, etc. Su función es proveer de los métodos y propiedades necesarios para realizar una correcta conexión con el origen de datos. Este objeto es común tanto para el modo Conectado como para el modo Desconectado. Recomendaciones a la hora de realizar las conexiones: 1. Para un óptimo rendimiento de la aplicación, lo ideal es usar el proveedor nativo del Servidor de Base de Datos con el que vamos a trabajar. 2. La apertura de la conexión es lo último que hay que hacer. Es decir, todas las variables que se puedan definir antes de la conexión, se definirán. 3. La conexión debe de cerrarse lo antes posible, obviamente se cerrará si no se utilizará más adelante. 4. Nunca se debe dejar una conexión sin cerrar. 5. Todas las conexiones hay que realizarlas bajo Try..Catch.. La mejor elección del modo de trabajo dependerá del entorno y las necesidades y requerirá un estudio detallado de cada situación. Normalmente se realizarán distintas pruebas para verificar el nivel de respuesta de la aplicación. Como hemos visto, ADO puede trabajar tanto en modo permanentemente conectado a la base de datos, como en modo desconectado (conecta sólo cuando es necesario). A continuación vemos en detalle los dos modos de trabajo de ADO.

Capítulo 4. Programación en ADO.

Página 40

4.6. El modo ADO Conectado
El modo conectado mantiene la conexión con el servidor, mientras la parte de la aplicación que ha accedido a los datos este activa. El modo conectado tiene un esquema de trabajo que podemos ver en el siguiente esquema:

Esquema ADO Conectado
Aplicación Origen Datos DataReader Command Connection

Representación en código del esquema
string Conexion = "Provider=Microsoft.Jet.OLEDB.4.0;" + "Data Source=C:.\\..\\..\\empresa.mdb;"; OleDbConnection objConexion = new OleDbConnection(Conexion); string Consulta = "SELECT nombre, apellidos FROM empresa"; OleDbCommand objComando = new OleDbCommand(Consulta, Conexion); OleDbDataReader Informacion = objComando.ExecuteReader();

Una vez ejecutado un objeto Connection, podemos ejecutar comandos de SQL por mediación del objeto Command, que al igual que Connection, varía en función del Servidor de Datos. Para OleDb tenemos OleDbCommand, para ODBC, tenemos OdbcCommand, para Sql Server tenemos SqlCommand y para Oracle, tenemos OracleCommand. Muchas veces no es necesario almacenar los datos leídos en un Conjunto de Datos (DataSet), ya que esta opción puede consumir mucha memoria. En su lugar podemos usar el objeto DataReader. Este objeto, vuelca directamente la información a la aplicación, pero la trae en pequeñas unidades de trabajo que descarga la memoria de la máquina cliente. Al igual que para los objetos anteriores, disponemos de un objeto concreto para cada origen de datos. Así, tenemos OleDbDataReader para OleDb, SqlDataReader para Sql Server, OdbcDataReader para ODBC y OracleDataReader para Oracle. Ejemplo: Uso ADOConectado-SQL y Uso ADOConectado-ACCESS
Capítulo 4. Programación en ADO. Página 41

El Modo Conectado usa un objeto del tipo DataReader para recuperar un conjunto de datos desde la base de datos y posteriormente los almacena en un buffer intermedio (aunque los datos no son almacenados en ninguna memoria cache). Un DataReader necesita obligatoriamente un objeto Command para poder trabajar. El resultado de la ejecución de un objeto Command es utilizado por el DataReader para crear ese buffer intermedio. Para poder cargar el DataReader, ExecuteReader() del objeto Command: DataReader = Command.ExecuteReader(), La propiedad HasRows del DataReader permite saber si existen filas en el objeto DataReader después de ejecutar el Command.ExecuteReader(); Ejemplo:
// Crear la conexión con la base de datos Empleados de //Access string objConexion = "Provider=Microsoft.Jet.OLEDB.4.0; Data “ + “Source=C:.\\..\\..\\empleados.mdb;";

aplicamos

el

método

OleDbConnection objConexionOleDb = new OleDbConnection(objConexion); //Crear una consulta para la base de datos string strConsulta = "SELECT nombre, apellidos FROM Empleados"; OleDbCommand objOrden = new OleDbCommand(strConsulta, objConexionOleDb); // Abrir la base de datos objConexionOleDb.Open(); // ExecuteReader hace la consulta y devuelve un OleDbDataReader OleDbDataReader objLector = objOrden.ExecuteReader(); // Usamos el método Read para acceder a los datos. HasRows // permite saber si hay registros cargados desde la consulta. if (objLector.HasRows) { while (objLector.Read()) // siguiente registro MessageBox.Show(objLector["nombre"] + " " + objLector["apellidos"]); } // Llamar siempre a Close una vez finalizada la lectura objLector.Close();

Método Read. Se utiliza el método Read del objeto DataReader para obtener una fila a partir de los resultados de una consulta. Para obtener su contenido, podemos acceder a cada columna o campo contenido en el DataReader, por su nombre (como hemos realizado en el ejercicio anterior – nombre y apellidos-) o por su referencia numérica. Es decir, en el ejemplo,
Capítulo 4. Programación en ADO. Página 42

podemos sustituir objLector[“nombre”], por objLector[0]. Esto es porque la columna que se encuentra en la posición 0 del DataReader es la de Nombre. De todas formas, el mejor rendimiento se consigue usando los métodos que dispone el DataReader y que permiten tener acceso a los valores de las columnas en sus tipos de datos nativos (GetDateTime, GetDouble, GetString, GetGuid, GetInt32, etc.). Si se utilizan los métodos de descriptor de acceso con tipo, dando por supuesto que se conoce el tipo de datos subyacentes, se recude el número de conversiones de tipo necesarias para recuperar el valor de una columna. Para el ejemplo anterior, podemos sustituir la línea que escribe los datos por esta otra:
MessageBox.Show(objLector.GetString(0) + " " + objLector.GetString(1));

Obsérvese que entre los paréntesis del método de descriptor de acceso con tipo GetString, hay que indicar la referencia numérica de la columna a la que nos estamos refiriendo. Hay que tener cuidado si el campo al que se accede es null, el uso de GetString da un error. Los DataReader, cuando se han terminado de utilizar, es necesario llamar al método Close() para su cierre, ya que si el objeto Command contiene parámetros de salida o valores devueltos, éstos no estarán disponibles hasta que se cierre el DataReader. Un DataReader usa de forma exclusiva el objeto Connection, por tanto, no se podrá ejecutar ningún comando para el objeto Connection hasta que se cierre el DataReader original, incluida la creación de otro DataReader. Un DataReader dispone del método GetSchemaTable(), que permite recuperar información sobre el esquema actual de datos. El resultado lo devuelve en un objeto del tipo DataTable, rellenando cada fila por cada una de las columnas del conjunto de datos. Cada Columna de una fila de la tabla de esquemas está asociada a una propiedad de la columna que se devuelve en el conjunto de datos. ColumnName es el nombre de la propiedad y el valor de la Columna es el de la propiedad. Ejemplo:
DataTable obj = objLector.GetSchemaTable(); foreach (DataRow fila in obj.Rows) { foreach (DataColumn columna in obj.Columns) { Console.WriteLine(columna.ColumnName + " = " + fila[columna]); } }

Capítulo 4. Programación en ADO.

Página 43

Ejecutar un comando insertar en ADO Conectado Cuando hacemos consultas usando el objeto Command de ADO Conectado, le suministramos la sentencia como un string y a continuación ejecutamos el comando con ExecuteReader. Pero si lo que deseamos es realizar otros comandos que necesitan parámetros, como ocurre en el caso del comando Insert que necesita una lista con los valores a insertar, tenemos que utilizar la colección Parameters del objeto Command y luego podemos ejecutar el comando pero con el método ExecuteNonQuery. ExecuteReader devuelve en un objeto DataReader el resultado de la consulta. Para el caso de ExecuteNonQuery, lo que devuelve es un valor de tipo int con el número de filas afectadas por el comando ejecutado. Ejemplo:
SqlConnection myConn = new SqlConnection("Data Source=PORTATIL\\SQLEXPRESS;Integrated Security=True;database=Empleados"); string str = "INSERT INTO empleados(NombreEmpleado, ApellidosEmpleado)” + "VALUES(@Nombre, @Apellidos)"; SqlCommand myCommand = new SqlCommand(str, myConn); myCommand.Parameters.AddWithValue("@Nombre", "Pedro"); myCommand.Parameters.AddWithValue("@Apellidos", "López Ruiz");

Como podemos ver, str, contiene la cadena de SQL que se tiene que ejecutar en el servidor. En la cláusula “values” aparecen variables de SQL que son las que guardarán el valor pasado con la propiedad, Parameters, del objeto Command. El método AddWithValue, nos permite cargar en una sola acción el parámetro y su valor. Otra posibilidad, menos académica, es pasar los valores a la sentencia como podemos ver en este otro ejemplo:
string str2 = "INSERT INTO empleados(NombreEmpleado, ApellidosEmpleado, DNIEmpleado, EmpleadoActivo, IdDepartamento, IdCategoria) " + "VALUES(" + Nombre + "," + Apellidos + "," + DNI + "," + Activo + "," + Depar + "," + Cate + ")"; SqlCommand myCommand = new SqlCommand(str2, myConn);

Las variables como Nombre, Apellidos, etc, deben de contener el valor entre comillas simples. Por ejemplo: string Nombre = “ ‘Antonio’ “. NOTA: Esto permite un fallo de seguridad conocido como SQL inyectado. Esta última forma, aunque pueda parecer práctica no se debe de realizar nunca.

Capítulo 4. Programación en ADO.

Página 44

Ejecutar un procedimiento almacenado con parámetros de salida. Debido a temas de rendimiento del servidor de SQL, la mayor parte de los accesos a los datos no se suelen realizar por mediación de instrucciones ejecutadas directamente, sino que se utilizan procedimientos almacenados. Aquí vamos a diseñar un ejemplo que accede a un procedimiento almacenado llamado ActualizaPrecios, que tomando dos parámetros de entrada: Codigo y Precio, actualiza el precio para el Id de libro facilitado por código. El procedimiento devuelve con un parámetro de salida llamado result, el número de filas afectadas. En principio sólo será una (el Id del libro es Clave Primaria y por tanto no acepta repeticiones). Lo primero que tenemos que saber es que en ADO es necesario crear tanto los parámetros de entrada como el parámetro de salida con el mismo nombre que tienen en el procedimiento almacenado. Ejemplo Uso ProcedimientoAlmacenado
SqlCommand objComando = objConexion.CreateCommand(); objComando.CommandText = "actualizaprecios"; objComando.CommandType = CommandType.StoredProcedure; objComando.Parameters.Add(new SqlParameter("@result", SqlDbType.Int)); objComando.Parameters.Add(new SqlParameter("@Codigo", "1")); objComando.Parameters.Add(new SqlParameter("@Precio", "167")); objComando.Parameters[0].Direction = ParameterDirection.Output; objComando.ExecuteNonQuery();

Hay que indicar que el objeto que aparece en este ejemplo, objConexion, es un objeto del tipo Connection que realiza la conexión con la base de datos. Por mediación de la propiedad CommandText introducimos el nombre del procedimietno almacenado. A continuación y, por mediación de la colección Parameters del objeto Command, introducimos los tres parámetros: uno de salida (result) y dos de entrada (Codigo y Precio). En los parámetros de entrada especificamos el valor que van a tener cuando se ejecute el procedimiento. Dado que el primer parámetro declarado en el objeto Command ha sido el valor de salida, cuando queremos invocarlo llamamos a la colección Parameters[0]. Sabemos que está en la posición cero. Observamos que por mediación del miembro enumerado SqlDbType, especificamos en el parámetro de salida el tipo correspondiente al valor devuelto por el procedimiento almacenado.

Capítulo 4. Programación en ADO.

Página 45

4.7. El modo ADO Desconectado
La norma general es usar un adaptador por cada origen de datos y un solo objeto DataTable del conjunto de datos.

Esquema ADO Desconectado

DataAdapter Aplicación Conjunto de Datos
SelectCommand InsertCommand DeleteCommand UpdateCommand

Origen de Datos

Conexión

El Modo Desconectado. Siguiendo la estructura explicada anteriormente, para trabajar con ADO desconectado necesitamos los siguientes objetos: Connection, DataAdapter y DataSet. La Clase DataSet Si bien el DataSet no forma parte del ADO como tal, ya que podemos usar un DataSet sin necesidad de conectarnos a una base de datos, forma parte importante del modelo de ADO Desconectado.

Jerarquía de la clase DataSet
DataTable DataRelation DataTable

Datacolumn

Datacolumn

DataRow

DataRow

Un DataSet es una base de datos de memoria interna. Por mediación del objeto DataTable puede contener en su interior múltiples tablas compuestas por campos de distintos tipos, al igual que si estuviéramos en SQL. Como tablas que son, las filas pueden ser relacionadas con otras tablas, a través de claves exteriores y de relaciones complejas que imponen restricciones de datos
Capítulo 4. Programación en ADO. Página 46

padre/hijo. Los DataTable permiten asignar valores automáticos según se insertan filas en la tabla, al igual que hacen los campos Identity en SQL Server. En un DataSet se pueden utilizar métodos para buscar el contenido de las tablas que lo componen. Además se puede tratar un DataSet como si fuera un documento XML y realizar consultas XPath. Evidentemente un DataSet puede guardar información en formato XML y en un formato binario propio para ADO .Net. Podemos ver un ejemplo de cómo se crea un DataSet de forma dinámica, así como las opciones de agregar, eliminar y consultar filas contenidas en un DataTable. Ejemplo Uso DataSet En el ejemplo de Uso DataSet podemos comprobar como el propio DataSet es capaz de guardar tablas, por mediación del objeto DataTable, como si se tratase de un gestor de SQL. En este ejemplo hemos diseñado una simple tabla que contenía un campo clave y hemos realizado una búsqueda sobre la tabla. ADO Desconectado. Ejemplo utilizando la interfaz gráfica de Visual Studio. En este caso vamos a utilizar a modo de ejemplo los componentes disponibles en la barra de herramientas. Empezamos con el objeto SQLDataAdapter. Si introducimos un objeto SQLDataAdapter (realizaremos la conexión contra SQL Server), se lanzará de forma automática la siguiente ventana:

Capítulo 4. Programación en ADO.

Página 47

El hecho de utilizar el SQLDataAdapter, permite definir en el mismo proceso el objeto SQLConnection. Pulsamos una conexión existente o nueva.

En la imagen superior, en el objeto DataAdapter, podemos definir todas las propiedades necesarias para poder realizar una conexión. Se puede especificar un Origen de Datos distinto usando para ello el botón “Cambiar…”. Nos permite especificar cuatro orígenes distintos: tres en modo nativo (el de Access es OleDb) y uno en modo ODBC, que es un estándar. Seleccionamos en la ventana “Agregar Conexión”, Origen de Datos SQL Server. A continuación seleccionamos el nombre del Servidor donde se encuentran la Base de Datos que queremos consultar. Especificamos “Usar Autenticación por Windows” (si el servidor requiriera conexión por usuario y contraseña de SQL Server, habría que introducirlas en estas casillas). A continuación seleccionamos la Base de Datos de la lista desplegable. En nuestro caso seleccionamos “Telefonos” (utilizar el fichero de Generar la base de datos de teléfono en caso de no estar en el servidor). Después, en el menú “Elegir un Tipo de Comando”, seleccionamos “Usar Instrucciones SQL”. Para facilitarnos la escritura de la sentencia de SQL, disponemos de un asistente en el botón “Generador de Consultas”. Después de seleccionar en la interfaz gráfica la instrucción, pulsamos Aceptar. Devuelta a la ventana de “Elegir un Tipo de Comando”, pulsamos el botón “Siguiente”, no el de “Finalizar”. Aparecerá una pantalla explicando que todas las instrucciones para el adaptador se han realizado de forma correcta. Ahora sí, pulsamos el botón de “Finalizar”.

Capítulo 4. Programación en ADO.

Página 48

Crear el Conjunto de Datos. Hasta el momento, sólo hemos definido la conexión, la tabla o tablas de las que obtendremos los datos y hemos generado las instrucciones necesarias para ejecutar la consulta sobre la Base de Datos. Ahora debemos generar el resultado de la consulta. Para ello usaremos en el menú “Datos”, del Visual Studio, la opción de “Generar conjunto de Datos”. En nuevo tecleamos el nombre que tendrá el DataSet que utilizaremos para rellenar los datos (ver imagen de la derecha) y pulsamos aceptar. Si lo que queremos es incluir una consulta en una nueva tabla (DataTable) dentro de un DataSet ya creado, seleccionamos en “Existente” el nombre del DataSet. A continuación incorporamos al formulario un objeto del tipo DataGridView, que será el responsable de controlar y editar los datos que nos son suministrados por el DataSet. En el DataGridView, pulsamos sobre la flecha que aparece en la parte superior derecha del Grid, cuando lo seleccionamos. En “Elegir origen de Datos”, (ver imagen de la derecha) seleccionamos, según el ejemplo, “dataSetTelefonos1”. Esto hace, que la propiedad DataSource del DataGridView, sea definida con el nombre del DataSet. A continuación seleccionamos la tabla Telefonos en la propiedad DataMember del DataGridView.

Capítulo 4. Programación en ADO.

Página 49

Ahora falta cargar el DataGridView con los datos disponibles, para ello, incorporamos un botón al formulario que servirá para mostrar los datos en el Grid y en su evento click, tecleamos el siguiente código.
//Limpia cualquier contenido que tuviera. dataSetTelefonos1.Clear(); //Después llamamos al adaptador y a su método Fill, para //que cargue los datos en el DataSet. sqlDataAdapter1.Fill(dataSetTelefonos1);

Un DataGridView, permite modificar los datos que se encuentran en las filas. Si no queremos que se pueda modificar, debemos de poner su propiedad ReadOnly a true. Pero a veces, es muy cómodo para el usuario modificar directamente los datos en un DataGridView y debemos dar esa posibilidad al usuario. En el caso de las conexiones con ADO, podemos modificar el contenido de los datos obtenidos con el DataAdapter que se encuentran en el DataGridView, pero estos no se actualizaran en la Base de Datos hasta que no se lo indiquemos. Para guardar los datos, escribiremos este código en otro botón que utilizaremos para que el usuario cuando quiera pueda guardar:
// Primero pregunta si se han producido cambios en el // Conjunto de Datos contenidos en el DataSet. if (dataSetTelefonos1.HasChanges()) { if (MessageBox.Show("Quiere Modificar los cambios en la Base de Dato (Sí o No)", "Modificar Datos", MessageBoxButtons.YesNo) == DialogResult.Yes) { sqlDataAdapter1.Update(dataSetTelefonos1); MessageBox.Show("Datos Actualizados"); } }

ADO Desconectado usando código en lugar del asistente. Si bien el entorno de diseño es muy cómodo de usar, en el modelo de programación de tres capas, la parte de conexión a Bases de Datos, es sin lugar a dudas Capa Lógica de Negocio o Intermedia. Por tanto, no es muy buena idea usar los asistentes para su desarrollo, ya que estos acaban en la Capa Presentación que es la ventana. En su lugar desarrollaremos clases cuya finalidad sea generar los datos desde la Base de Datos. En el modo ADO Conectado ya vimos algunos ejemplos de cómo desarrollarlo en modo código. El ejemplo que viene a continuación tiene como finalidad desarrollar las cuatro instrucciones básicas para trabajar con datos desde ADO Desconectado: Select, Insert, Update y
Capítulo 4. Programación en ADO. Página 50

Delete. Para ello, en lugar de utilizar el objeto Command, usaramos el objeto CommandBuilder:
string Cadena = @"Data Source=PROFESOR\SQLEXPRESS;Initial Catalog=Empresa;Integrated Security=True"; SqlConnection Conexion = new SqlConnection(Cadena); SqlDataAdapter Adaptador = new SqlDataAdapter("SELECT * from categorias", Conexion); Adaptador.MissingSchemaAction = MissingSchemaAction.AddWithKey; SqlCommandBuilder RestoSQL = new SqlCommandBuilder(Adaptador); Adaptador.InsertCommand = RestoSQL.GetInsertCommand(); Adaptador.DeleteCommand = RestoSQL.GetDeleteCommand(); Adaptador.UpdateCommand = RestoSQL.GetUpdateCommand(); DataSet DatosDataSet = new DataSet(); Adaptador.Fill(DatosDataSet); DataRow FilaNueva = DatosDataSet.Tables[0].NewRow(); FilaNueva[1] = textBox1.Text; FilaNueva[2] = textBox2.Text; DatosDataSet.Tables[0].Rows.Add(FilaNueva); Adaptador.Update(DatosDataSet);

En este ejemplo, cabe destacar la propiedad MissingSchemaAction, cuya finalidad es añadir las columnas y columna clave necesarias para operar cuando no existan un DataTable y DataSet que lo contenga. En nuestro caso, es necesario crear antes las instrucciones que generar el DataSet y por supuesto el DataTable. Como podemos ver el SqlCommandBuilder recibe como parámetro de entrada (usamos el constructor) al propio adaptador y, a partir del adaptador que tiene fijada la instrucción Select, usamos los métodos GetInsertCommand, GetDeleteCommand y GetUpdateCommand, para construir las otras tres instrucciones. El resto es llamar al método Fill del adaptador para que cargue el DataSet con la tabla generada por la instrucción Select y, a continuación, insertar una fila en el DataTable, para posteriormente actualizar todos los datos con el método Update del adaptador.

Capítulo 4. Programación en ADO.

Página 51

4.8. Control BindingSource
Es un objeto que hace de intermediario entre el control y el conjunto de datos. Simplifica la conexión facilitando la actualización del contenido, la notificación de cambios, etc. Se incluye la navegación, ordenación, filtrado y actualización. El origen de datos subyacente se fija a través de uno de los siguientes mecanismos: Usar el método Add para añadir un elemento al componente BindingSource. Asignar a su propiedad DataSource una lista, objeto o un tipo.

Su funcionamiento permite enlazar universalmente todos los controles de formularios Windows a orígenes de datos muy diversos. Ejemplo Uso BindingSource. Para poder enlazar una matriz de tipo List, a un objeto DataBinding y posteriormente mostrar todos los datos de esa matriz en dos TextBox llamados: tbxNombre y tbxApellidos, usando para ello dos botones (siguiente, anterior):
private List<Persona> MatrizPersonas = new List<Persona>(); private BindingSource Navegador = new BindingSource(); //Asignamos al BindingSource, el origen de datos. Navegador.DataSource = MatrizPersonas; //Vinculamos las cajas de texto al control BindingSource. tbxNombre.DataBindings.Add("Text", Navegador, “NombrePersona"); tbxApellidos.DataBindings.Add("Text", Navegador, "ApellidosPersona");

A continuación diseñamos los eventos click de los dos botones:
private void btnSiguiente_Click(object sender, EventArgs e) { Navegador.MoveNext(); } private void btnAnterior_Click(object sender, EventArgs e) { Navegador.MovePrevious(); }

Si en lugar de asignar el DataBinding para controlar una matriz quisiéramos hacerlo para acceder a un conjunto de datos almacenado en un DataSet:
tbxNombre.DataBindings.Add("Text", DataSetPersonas, "Personas.Nombre");

Previamente se ha asignado al DataBinding el DataSet (DataSetPersonas). “Personas.Nombre”, Personas es la tabla que contiene el DataSet y Nombre es el campo de la tabla.
Capítulo 4. Programación en ADO. Página 52

4.9. Control BindingNavigator
El control BindingSource, provee de varias funcionalidades para trabajar con conjuntos de datos. A partir de la versión 2005, Microsoft incorporó un nuevo objeto llamado BindingNavigator, que permite realizar funciones de navegación por el conjunto de datos. Estas funciones son: Primer registro, último, siguiente, anterior, número de registros totales en el conjunto de datos y posición actual. Ejemplo: Uso BindingNavigator Es un control basado en un objeto ToolStrip que incluye los componentes mencionados anteriormente. Necesita de un objeto BindingSource para funcionar, el cual se vincula al objeto BindingNavigator por mediación de su propiedad BindingSource. Práctica para crear un BindigNavigator: 1. Creamos un objeto SQLDataAdapter. Seleccionamos el origen de datos y definimos la consulta que se realizará sobre la base de datos empleados. Nuestro objetivo es obtener todos los registros de empleados mostrados por IdEmpleado, NombreEmpleado, ApellidosEmpleado, Nombre del Departamento Empleado y Nombre de la Categoría del Empleado. 2. Vamos al menú Datos y generamos el Conjunto de Datos. Lo Llamamos objDataSetEmpleados. Automáticamente genera un objeto, cuyo nombre es el que le hemos suministrado pero al final le añade un 1 (objDataSetEmpleados1). 3. Incluimos en el formulario los Textbox y label que contendrán la información. 4. Incluimos un objeto BindingSource, llamado BindingSource1, al cual le asignamos a su propiedad DataSource, el objeto objDataSetEmpleados1 y a su propiedad DataMember, el nombre de la tabla que va a manejar. 5. Incluimos un objeto BindingNavigator, al cual le asignamos a su propiedad BindingSource, el objeto BindingSource1. 6. A continuación seleccionamos la propiedad Text del DataBindings de cada Textbox y le asignamos el campo de la tabla que se mostrará. Usamos para ello los campos contenidos en el BindingSource1. Ver figura. 7. Es necesario cargar el DataSet Previamente. Para ello podemos utilizar el evento Load de la ventana en la cual introducimos las siguientes líneas:
objDataSetEmpleados1.Clear(); sqlDataAdapter1.Fill(objDataSetEmpleados1); Capítulo 4. Programación en ADO. Página 53

4.10. Programación avanzada en ADO
4.10.1. Transacciones. Operaciones de RollBack en la Base de Datos. Espacio de nombres: System.Transactions Microsoft incluyó a partir de Visual Studio 2005 un nuevo espacio de nombres, System.Transactions, que proporciona una medio unificado por el cual podemos acceder a recursos transaccionales, tales como la base de datos SQL Server y la herramienta de Microsoft para realizar el manejo de colas de mensajes MSMQ(Microsoft Message Queues). Una de las ventajas de estas clases es que la aplicación puede utilizar recursos locales de bajo coste durante una ejecución y luego autoincrementarse utilizando operaciones distribuidas la próxima vez que se ejecute, basadas en los recursos a los que el usuario final accede. El nuevo sistema de transacciones proporciona dos formas de hacer uso de transacciones como un cliente: explícitamente, utilizando derivados de la clase Transaction e, implícitamente, usando los ámbitos de transacciones. 4.10.1.1. Transacciones Explícitas. Lo primero a tener en cuenta es que es necesario agregar como referencia al proyecto el espacio de nombres System.Transactions. Este espacio de nombres nos va a permitir usar la clase CommitableTransaction, que alberga los métodos Commit y Rollback, utilizados para ejecutar la transacción o deshacer la transacción. La operación queda vinculada a la conexión, lo que significa que estos dos métodos sólo actúan sobre la conexión a la que son vinculados. Ejemplo Uso TransaccionesExplicitas. Como podemos ver en el ejemplo, si el borrado de las filas que cumplen la condición se encuentra con el método Rollback, a pesar de que al ejecutar el Comando con el método ExecutenonQuery a provocado el borrado, la operación es anulada. Por el contrario si colocamos el método Commit, la operación quedará ejecutada. 4.10.1.2. Transacciones Implícitas. Microsoft recomienda realizar transacciones implícitas en lugar de transacciones explícitas. La razón es que cuando se usan transacciones implícitas, las operaciones desde múltiples administradores de recursos pueden coexistir y operar unas con otras conforme a reglas predefinidas. Si utilizamos transacciones implícitas, el código puede automáticamente alistarse en una transacción primaria si existe o puede crear una nueva transacción si es necesario. Mucho más útil es que si el código invoca a otro código que también usa transacciones implícitas, la transacción implícita no se acomete hasta que todas las transacciones anidadas decidan acometerse también.
Capítulo 4. Programación en ADO. Página 54

Las transacciones implícitas se realizan mediante la clase TransactionScope., que tiene un uso muy parecido a using. El código que reside dentro de un ámbito de transacción es intrínsecamente transaccional, y accede a los administradores de recursos compatibles (como SQL Server 2005) desde dentro de un ámbito de transacción que utilizará transacciones implícitamente. Si el código no llega a la línea donde se llama al método Complete en un TransactionScope, la transacción creada dentro del ámbito se eliminará y las transacciones primarias utilizadas a través del nido se nominarán para eliminarse. Ejemplo Uso TransaccionesImplicitas Como podemos ver en el ejemplo, por mediación de un using hemos declarado un ámbito de transacción implícita. Si el código dentro del using no se llega a ejecutar en su totalidad porque hay un error, al no llegar a la sentencia AmbitoTransaccion.Complete(), no se ejecutará ninguna orden que se hubiera efectuado sobre el servidor de SQL. Ejemplo Uso TransaccionesAnidadas
static SqlConnection objConexion; private void btnAceptar_Click(object sender, EventArgs e) try { using (TransactionScope scope = new TransactionScope()) { using (objConexion = new SqlConnection("data source=portatil\\sqlexpress; initial catalog=Empresa; Integrated Security=SSPI;")) { objConexion.Open(); for (int x = 1; x < 8; x++) { DeleteEmpleado(x); } } scope.Complete(); } } catch (TransactionAbortedException) { MessageBox.Show("La operación ha sido abortada porque alguna de las transacciones ha fallado"); } } private void DeleteEmpleado(int IdEmpleado) { using (TransactionScope scope = new TransactionScope()) { SqlCommand objComando = objConexion.CreateCommand(); objComando.CommandText = "DELETE Empleados WHERE IdEmpleado = " + IdEmpleado.ToString(); objComando.ExecuteNonQuery();

Capítulo 4. Programación en ADO.

Página 55

if (IdEmpleado < 5) scope.Complete(); } }

Quizás donde se pueda obtener un mayor rendimiento de las Transacciones sea en la posibilidad de deshacer transacciones anidadas, donde una de ellas provoca un error, o antes de que se ejecute se produce un problema, y es necesario deshacer todo lo anterior. En este ejemplo se crea un ámbito primario y en su interior un bucle que se itera ocho veces; cada iteración intenta eliminar un empleado. El método DeleteEmpleado eliminará todos los empleados excepto aquellos que su IdEmpleado sea mayor de 5. Cada una de las iteraciones llamó al método Complete de la transacción y se ejecutó perfectamente, pero si un Id sobrepasa el valor de cinco, la transacción se anula. Cuando llamamos a un Id que es mayor de cinco, la iteración falló ya que no se llamó a su método Complete y es está iteración la culpable de que se provoque la llamada a la excepción TransactionAbortedException. Esto tiene como consecuencia que el ámbito primario se detenga y su método Complete nunca se cumpla.

4.10.2. Acceso Asíncrono a la Base de Datos. Antes de Net Framework 2.0, si teníamos que acceder a una base de datos con un tiempo de espera para realizar la consulta muy alto, y queríamos que la aplicación no quedara suspendida, la llamada a la base de datos y la consulta a ejecutarse se incluían en un método que era lanzado en segundo plano (subprocesamiento). A partir de Net Framework 2.0 Microsoft implemento métodos en muchos objetos cuya finalidad es precisamente ejecutarse en hilos independientes, al margen de la aplicación principal, para de esta forma liberar a nuestra aplicación de procesos complejos. El caso que vamos a tratar es el del objeto SqlCommand, que incorpora a los métodos: ExecuteNonQuery, ExecuteReader y ExecuteXmlReader la posibilidad de ejecutarse en segundo plano por mediación de Begin/End. De esta forma podemos ejecutar los métodos en modo asíncrono, mientras la aplicación sigue respondiendo a otros eventos que se pudieran producir. Ejemplo: Uso ADOAsincrono

Capítulo 4. Programación en ADO.

Página 56

Lo primero que podemos ver en el ejemplo es que la cadena de conexión incorpora un parámetro que permite ejecutar el comando en segundo plano:
SqlConnection conn = new SqlConnection("data source=(localhost); initial catalog=Empresa; Integrated Security=SSPI; Asynchronous Processing=true;");

El parámetro Asynchronous Processing a true le indica al objeto ADO que esta conexión utilizará subprocesamiento múltiple. Si no se activa este parámetro no lo permitirá y cuando se llame al método dará un error. El método BeginExecuteReader inicia el proceso de ejecución asincrónica de una instrucción de Transact-SQL o de un procedimiento almacenado que devuelve filas, para que otras tareas puedan ejecutarse simultáneamente mientras se ejecuta la instrucción. Cuando termina la instrucción se debe de llamar al método EndExecuteReader para finalizar la operación y recuperar el valor de SqlDataReader devuelto por el comando. El método BeginExecuteReader devuelve un valor inmediatamente, pero hasta que el código ejecute la llamada al método EndExecuteReader correspondiente, no debe ejecutar ninguna otra llamada que comience una ejecución sincrónica o asincrónica con respecto al mismo objeto SqlCommand. Es decir, el objeto declarado en una llamada asíncrona no puede utilizarse nuevamente hasta que la llamada finalice. Si se llama a EndExecuteReader antes de que finalice la ejecución del comando, el objeto SqlCommand se bloqueará hasta que la ejecución termine. En la sintaxis de la llamada que hemos realizado en el ejemplo tenemos:
public IAsyncResult BeginExecuteReader (AsyncCallback callback, Object stateObject)

El método asíncrono BeginExecuteReader, devuelve un valor del tipo IAsyncResult, que se puede utilizar para sondear y/o esperar los resultados; este valor también es necesario al invocar a EndExecuteReader, que devuelve una instancia de SqlDataReader que se puede usar para recuperar las filas devueltas. Como parámetros de entrada tenemos: CallBack. El parámetro callback permite especificar un delegado de AsyncCallback al que se llama una vez finalizada la instrucción. Se puede llamar al método EndExecuteReader desde este procedimiento de delegado o desde cualquier otra ubicación de la aplicación. Además, se puede pasar cualquier objeto en el parámetro stateObject y el procedimiento de devolución de llamada puede recuperar esta información mediante la propiedad AsyncState.

Capítulo 4. Programación en ADO.

Página 57

Observe que el texto del comando y los parámetros se envían al servidor de forma sincrónica. Si se envía un comando de gran tamaño o un gran número de parámetros, este método puede bloquearse durante las operaciones de escritura. Una vez enviado el comando, el método devuelve un valor inmediatamente sin esperar una respuesta del servidor; es decir, las lecturas son asincrónicas. Aunque la ejecución de comandos es asincrónica, la obtención de valores sigue siendo sincrónica. Esto significa que las llamadas a Read pueden bloquearse si se requieren más datos y se bloquea la operación de lectura de la red subyacente. Dado que el procedimiento de devolución de llamada se ejecuta desde un subproceso de fondo proporcionado por el motor en tiempo de ejecución de Microsoft .NET, es muy importante que se controlen rigurosamente las interacciones entre los subprocesos desde las aplicaciones. Por ejemplo, no se debe interactuar con el contenido de un formulario desde el procedimiento de devolución de llamada. En caso de que se tenga que actualizar el formulario, se debe volver al subproceso del formulario. Todos los errores que aparecen durante la ejecución de la operación se inician como excepciones en el procedimiento de devolución de llamada. Las excepciones se deben controlar en el procedimiento de devolución de llamada.

4.10.3. Ejecución de procesos por lotes En este caso vamos a utilizar un objeto DataSet para que sea el responsable de actualizar los datos en el SQL por mediación de un proceso por lotes. El ejemplo tiene tres partes muy concretas: la primera recupera los datos de la base de datos empresa, usando el DataSet (ADO Conectado); la segunda modifica filas existentes (las recuperadas en el DataSet), añade filas nuevas y elimina otras; y por último, se llama al método Update() del DataSet, que ejecuta todas las filas de la tabla dentro del DataSet y le aplica la instrucción correspondiente a su estado (añadir, borrar, insertar o modificar). Ejemplo: Uso ProcesoPorLotes Hay que tener en cuenta que los procesos por lotes tienen problemas de rendimiento cuando la cantidad de registros a modificar es muy grande. Cuando esto ocurre, el crear una instrucción SQL, ejecutarla y desplazarla al siguiente registro actualizado de la lista, añade una sobrecarga innecesaria a la operación de actualización. Ahora podemos establecer el tamaño del “lote de comando”. Si se establece en 0, el adaptador de datos utilizará la máxima capacidad de lote disponible. Si se determina un valor mayor de 1, el adaptador de datos realizará su actualización por lotes que contiene el número de instrucciones
Capítulo 4. Programación en ADO. Página 58

que previamente se indicó. El único tema importante es especificar la propiedad UpdateRowSource, con el valor de UpdateRowSource.None, en todos los comandos implicados en el proceso por lotes.

4.10.4. El uso de Link En LINQ to SQL se puede utilizar la tecnología LINQ para tener acceso a las bases de datos de SQL igual que se obtendría acceso a una colección en memoria. Conocemos a este modelo de programación como “LINQ” que proviene de .NET Language Integrated Query. Los desarrolladores podemos usar LINQ con cualquier fuente de datos. Podemos expresar consultas en los lenguajes de programación que se elijan y opcionalmente transformar/incrustar los resultados de las consultas en el formato que se quiera, para de esta forma manipular fácilmente los resultados. Los lenguajes habilitados para LINQ pueden aportar seguridad de tipos y chequeo en tiempo de compilación de las expresiones de consulta, y desarrollar herramientas que aporten intelisense y debugging cuando escriban código de LINQ. LINQ soporta un modelo de extensibilidad muy rico que facilita la creación de operadores eficientes para fuentes de datos. La versión 3.5 del .NET Framework viene con librerías que habilitan LINQ sobre objetos, XML y bases de datos. 4.10.4.1. LINQ to SQL. LINQ to SQL es una implementación de O/RM(Object Relational Mapping -Mapeador de Objetos Relacionales-) que viene con la versión 3.5 del .NET Framework, y nos permite modelar bases de datos relacionales con clases de .NET. Podemos consultar bases de datos con LINQ, así como actualizar, añadir y borrar datos de ellas. Modelando bases de datos con LINQ to SQL: Visual Studio 2008 viene con un diseñador de LINQ to SQL que nos aporta una forma fácil de modelar y visualizar una base de datos como un modelo de objeto de LINQ to SQL. Para utilizar el diseñador hay que agregar al proyecto un nuevo elemento llamado: “Clases de Link a SQL” Usando ese diseñador LINQ to SQL se puede crear fácilmente una representación de la base de datos “Northwind”.

Capítulo 4. Programación en ADO.

Página 59

El diseño de arriba define cuatro clases: Products, Categoryes, Order y OrderDetail. Las propiedades de cada clase mapean las columnas de cada table en la base de datos. Cada instancia de esa clase representa una fila en las tablas. Las flechas entre las cuatro clases de arriba representan las asociaciones/relaciones entre las diferentes entidades. Son típicamente modeladas como relaciones primary-key/foreign-key en la base de datos. La dirección de las flechas en el diseñador indican si la relación es uno-a-uno o uno-a-varios. Se añadiran propiedades fuertemente tipadas a las entidades basándose en esto. Por ejemplo, la clase Category de arriba tiene una relación de uno-a-varios con la clase Product. Esto implica que tendrá una propiedad “Categories” que es una colección de objetos Product con esa categoría. La clase Product entonces tiene una propiedad “Category” que apunta a una instancia de la clase Category representando la categoría a la que pertenece el producto. Esquema en el diseñador para la base de datos Northwing:

Entendiendo la clase DataContext Cuando pulsamos el botón “save” del diseñador de LINQ to SQL, Visual Studio generará clases .NET para representar las entidades y las relaciones de la base de datos que hemos modelado. Por cada archivo añadido a nuestra
Capítulo 4. Programación en ADO. Página 60

solución por el diseñador LINQ to SQL también se generará una clase DataContext. Esta clase es a través de la cual realizaremos las consultas a las entidades de nuestra base de datos. Esta clase tendrá propiedades que representarán a cada tabla que hemos modelado, así como métodos para cada procedimiento almacenado que añadamos. Por ejemplo, en la imagen de la izquierda podemos ver la clase DataClasses1DataContext, que se ha generado a partir de las tablas que hemos incluido en la vista de diseño. Además, podemos observar que ha generado una clase por cada una de las tablas que se han incorporado. Una vez diseñada la estructura, podemos realizar las operaciones que vienen a continuación: Consultar Datos:
DataClasses1DataContext objNort = new DataClasses1DataContext(); var objProductos = from p in objNort.Products where p.Categories.CategoryName == "Beverages" select p.ProductName; foreach (var obj in objProductos) Console.WriteLine(obj);

Modificar Datos:
DataClasses1DataContext objNoMo = new DataClasses1DataContext(); Products objPro = objNoMo.Products.Single(p => p.ProductName == "Tofu"); objPro.UnitPrice = 100; objPro.UnitsInStock = 100; objNoMo.SubmitChanges();

Insertar Datos:
DataClasses1DataContext objIn = new DataClasses1DataContext(); Products objProducto = new Products(); objProducto.ProductName = "Jamón"; objProducto.SupplierID = 3; objProducto.CategoryID = 2; objProducto.QuantityPerUnit = "18 - 500 g pkgs."; objProducto.UnitPrice = 20; objProducto.UnitsInStock = 100; objProducto.ReorderLevel = 0; objProducto.Discontinued = false; objIn.Products.InsertOnSubmit(objProducto);

Capítulo 4. Programación en ADO.

Página 61

//Guardamos los cambios en la base de datos objIn.SubmitChanges();

Borrar Datos:
//Borrar una fila usando Linq a SQL. DataClasses1DataContext objNoBo = new DataClasses1DataContext(); var objBorrar = from p in objNoBo.Products where p.ProductName.Contains("Jam") select p; if (objBorrar.Count() > 0) { objNoBo.Products.DeleteOnSubmit(objBorrar.First()); objNoBo.SubmitChanges(); }

Como hemos podido ver en cada uno de los ejemplos, salvo en el consultar, es necesario realizar un SubmitChanges para que los cambios realizados en la base de datos surtan efecto. Llamar a un procedimiento almacenado. Igual que realizamos operaciones sobre la base de datos con el equivalente en Linq a las instrucciones de SQL, también podemos llamar a procedimientos almacenados. La sintaxis es:
DataClasses1DataContext objNoBo = new DataClasses1DataContext(); var objProcedimiento = objNoBo.CustOrdersOrders(21);

En objProcedimiento almacena en una colección las filas devueltas por la ejecución del procedimiento llamado CustOrdersOrders. En este procedimiento, pasándole un Id de cliente (21), devuelve los pedidos realizados por el cliente. Al ser una colección podríamos usar un bucle foreach para recorrerla. 4.10.4.2. Linq a XML. Espacio de nombres System.XML.LINQ XML se ha adoptado ampliamente como un modo de dar formato a datos en diversos contextos. Por ejemplo, se puede encontrar XML en la web, en archivos de configuración, en bases de datos, etc. LINQ to XML es un método actualizado y rediseñado para la programación con XML. Proporciona capacidades de modificación de documento en memoria de Document Object Model (DOM), y es compatible con expresiones de consulta LINQ. Aunque estas expresiones de consulta difieren sintácticamente de XPath, proporcionan una funcionalidad similar. LINQ to XML simplifica el código XML al proporcionar una experiencia de consulta similar a SQL. Con un poco de estudio, los programadores
Capítulo 4. Programación en ADO. Página 62

acostumbrados a Transact-SQL, pueden aprender a escribir consultas concretas y eficaces en el lenguaje de programación que prefieran. Una de las ventajas de usar LINQ, es que permite escribir menos código, que a su vez resulte más expresivo, compacto y eficaz. Se pueden usar expresiones de consulta de distintos dominios de datos simultáneamente. LINQ to XML es una interfaz de programación XML en memoria y habilitada para LINQ que permite trabajar con XML desde los lenguajes de programación de .NET Framework. Se parece al Modelo de objetos de documento (DOM) en lo que respecta a la inserción del documento XML en la memoria. Puede consultar y modificar el documento; una vez modificado, puede guardarlo en un archivo o serializarlo y enviarlo a través de Internet. Sin embargo, LINQ to XML es diferente de DOM: proporciona un nuevo modelo de objetos más ligero con el que se trabaja más fácilmente y que aprovecha las mejoras de lenguaje de Visual C# 2008. La ventaja más importante de LINQ to XML radica en su integración con Language-Integrated Query (LINQ). Esta integración permite escribir consultas en el documento XML en memoria para recuperar colecciones de elementos y atributos. La capacidad de consulta de LINQ to XML es comparable en cuanto a funcionalidad (aunque no cuanto a sintaxis) a XPath y XQuery. La integración de Linq en Visual C# 2008 proporciona una escritura más rápida, comprobación en tiempo de compilación y una compatibilidad mejorada con el depurador. Otra ventaja es la capacidad de usar los resultados de la consulta como parámetros en constructores de objetos XElement y XAttribute, que habilita un método eficaz para crear árboles XML. Este método, denominado construcción funcional, permite que los desarrolladores transformen fácilmente árboles XML de una forma a otra. Los objetos básicos serían XElement y XDocument, cuyo funcionamiento y salvando el tema de las consultas en Linq, es similar a las clases aplicadas al modelo DOM en Net Framework. Ejemplo Uso LinqaXML:
//Documento XML generado por XElement. XDocument srcTree = new XDocument(new XComment("TComentario"), new XElement("Root", new XElement("Child1", "data1"), new XElement("Child2", "data2"), new XElement("Child3", "data3"), new XElement("Child2", "data4"), new XElement("Info5", "info5"), new XElement("Info6", "info6"), new XElement("Info7", "info7"), new XElement("Info8", "info8")));

Capítulo 4. Programación en ADO.

Página 63

//Documento XML generado por una consulta. XDocument doc = new XDocument( new XComment("Esto es un comentario en la consulta"), new XElement("Root", from el in srcTree.Element("Root").Elements() where ((string)el).StartsWith("data") select el)); Console.WriteLine(doc); doc.Save("c:\\fichero.xml"); Console.ReadLine(); XElement xmlTree1 = new XElement("Root", new XElement("Child", 1), new XElement("Child", 2), new XElement("Child", 3), new XElement("Child", 4), new XElement("Child", 5), new XElement("Child", 6)); XElement xmlTree2 = new XElement("Root", from el in xmlTree1.Elements() where ((int)el >= 3 && (int)el <= 5) select el); xmlTree2.Save("c:\\Elemento.xml"); Console.WriteLine(xmlTree2); Console.ReadLine();

4.10.5. Detectar Mensajes del Servidor de Datos en la aplicación. Muchas veces nos podemos encontrar ante la necesidad de mantener permanentemente actualizados unos datos que hemos solicitado al servidor. Una solución podría ser utilizar un Timer para que ejecute un proceso de consulta al servidor, por ejemplo, cada 2 segundos. Esta solución puede ser fácil de implementar en código, pero a buen seguro, que resulta muy costosa en recursos y tiempo en el servidor de datos si la consulta afecta a varias tablas y además es compleja. Una alternativa sería crear un trigger y una tabla complementaria que nos permitiera realizar una consulta muy simple para comprobar el estado de un valor en esa tabla complementaria. Por ejemplo, podemos crear una tabla y trigger como el que tenemos en este ejemplo:
CREATE TABLE dbo.TablaObservar (Version int not null) GO CREATE TRIGGER Observar ON dbo.Empleados AFTER INSERT, DELETE, UPDATE AS BEGIN SET NOCOUNT ON UPDATE dbo.TablaObservar SET Version = Version + 1 END GO

Capítulo 4. Programación en ADO.

Página 64

Este ejemplo crear una tabla llamada TablaObservar que contiene un campo llamado Version. Por mediación del Trigger, cuando se produzca un cambio sobre la tabla Empleados (alta, baja o modificación) se modificara el valor del campo sumando uno al valor que tuviera. Desde una aplicación en .Net podemos configurar un Timer para que nos facilite en una consulta el valor del campo Version de TablaObservar:
private void timer1_Tick(object sender, EventArgs e) { SqlConnection Conexion = new SqlConnection(Cadena); //Consulta que devuelve el valor del campo Version en TablaObservar SqlCommand Comando = new SqlCommand("SELECT TOP 1 Version FROM TablaObservar", Conexion); if (Conexion.State != ConnectionState.Open) Conexion.Open(); SqlDataReader Cambios = Comando.ExecuteReader(); if (Cambios.HasRows) { Cambios.Read(); int Version = (int)Cambios[0]; if (UltimaVersion != Version) { this.Text = Version.ToString(); CargarDatos(); UltimaVersion = Version; } } Cambios.Close(); }

En este código realizamos una consulta que nos devuelve sólo la primera fila de TablaObservar. Esa primera fila contiene el valor de Version. Cuando se produce sobre la tabla una modificación, alta o baja, el trigger programado suma uno al valor que tuviera ese campo. Como previamente hemos guardado el valor que tenía en la variable UltimaVersion, automáticamente detectará que los valores han cambiado y por tanto pasará a realizar una recargar de los datos que estamos visualizando en el DataGridView. De todas formas este sistema sigue realizando consultas al servidor, mucho menos pesadas y más rápidas, pero al fin y al cabo son cargas de trabajo para el servidor. El proveedor de SqlClient, dispone de una clase llamada SqlDependency que se puede utilizar para poner en vigilancia una consulta. Si los datos de la consulta sufren cambios, el Servidor notifica esos cambios. Para ello, el servidor de Base de Datos, debe de permitir el Service Broker y tenerlo activado. Uso NotifiacionCambiosSQLServer.

Capítulo 4. Programación en ADO.

Página 65

4.10.6. Programa objetos de espera en la ejecución de operaciones Muchas veces tenemos que ejecutar en el servidor procedimientos almacenados que tienen un alto coste en tiempo de ejecución. Al ejecutarse en el servidor, desde nuestra aplicación no podemos conocer el estado final del procedimiento. En la mayoría de los casos lo recomendable sería colocar una barra de progreso que nos vaya indicando el tiempo transcurrido y que el usuario conozca el tiempo apróximado que resta para terminar la ejecución. Podemos utilizar un sistema parecido al anterior con un objeto Timer para ir moviendo la barra y ralentizando su presentación según pasa el tiempo hasta conocer el fin del procedimiento. Pero es muy costo en recursos y ademas no representa claramente el avance en tiempo de ejecución del procedimiento. Para resolver el problema contamos con el sistema de notificaciones del objeto SqlConnection, que por mediación del evento InfoMessage, podemos detectar mensajes generados por el servidor de SQL con las instrucciones Raiserror y Print. Para probar un ejemplo del sistema de notificaciones, podemos crear el siguiente procedimiento almacenado en SQL:
CREATE PROCEDURE dbo.ProcedimientoLargo AS BEGIN SET NOCOUNT ON DECLARE @i INT SET @I = 1 WHILE @i<=10 BEGIN WAITFOR DELAY '00:00:01' --Simula una operación costosa en el SQL. DECLARE @msg NVARCHAR(50) SET @msg = 'Progreso: ' + CONVERT(nvarchar, @i) --Es importante colocar el WITH NOWAIT para que los mensajes --No se acumulen y se notifiquen al final. RAISERROR (@MSG, 1, 1) WITH NOWAIT SET @i = 1 + @i END END

Este ejemplo provoca una demora para simular que se está realizando una operación costosa en tiempo en el servidor. Es importante que el mensaje lanzado por Raiserror lleve la clausula With Nowait, que permite enviar el mensaje inmediatamente se ejecute la línea. Por defecto, los mensajes se acumularían y serían enviados al final de la ejecución del procedimiento almacenado. Este ejemplo cuenta hasta 10 y para. Cada vez que cuenta tiene una demora (Waitfor Delay) para simular que está trabajando. En cada pasada realiza una notificación para que sea capturada desde nuestro programa.

Capítulo 4. Programación en ADO.

Página 66

Para capturar los mensajes implementamos el siguiente código.
SqlConnection objConexion = new SqlConnection(); private void btnEjecutar_Click(object sender, EventArgs e) { BarraProgreso.Value = 0; objConexion.ConnectionString = @"Data Source=SERVIDOR;Initial Catalog=Empresa;Integrated Security=True"; //Programamos el evento InforMessage para el objeto Conexión. objConexion.InfoMessage += new SqlInfoMessageEventHandler(objConexion_InfoMessage); if (objConexion.State != ConnectionState.Open) objConexion.Open(); SqlCommand objProcedimiento = new SqlCommand("dbo.ProcedimientoLargo", objConexion); objProcedimiento.CommandType = CommandType.StoredProcedure; SqlDataReader objReader = objProcedimiento.ExecuteReader(); if (objReader.HasRows) while (objReader.Read()) { //Los resultados son procesados en este bloque de código. } } private void objConexion_InfoMessage(object sender, SqlInfoMessageEventArgs e) { //Avanza la progressBar en la cantidad especificada por la //propiedad Step BarraProgreso.PerformStep(); }

En este caso hemos programado el evento click de un botón que dispara la ejecución del procedimiento almacenado y un evento InfoMessage de la conexión que se lanza cuando un mensaje Raiserror es ejecutado dentro del procedimiento almacenado. En el InfoMessage simplemente llamamos al método PerformStep de la barra para que incremente su valor en la cantidad especificada. El valor de Step esta definido en 10.

Capítulo 4. Programación en ADO.

Página 67

Ejercicios básicos de ADO Conectado y ADO Desconectado: Ejercicio 1. Diseñar una aplicación que tomando la base de datos de Empresa en Access, llene un DataGridView con la información del Nombre, Apellido y DNI. Usar ADO Desconectado. Ejercicio 2. Tomando las bases de datos de libros y teléfonos, diseñar una aplicación que permita mostrar los datos de cada unas de ellas en un DataGridView diferente. Usar Access con ADO Desconectado. Ejercicio 3. Idem que el anterior pero usando Sql Server. Ejercicio 4. Usar la base de datos Empresa. Realizar una aplicación que manejando ADO Conectado, conecte con la Base de Datos de Empleados y permita modificar sus datos básicos como son, Nombre, Apellidos y DNI. Realizarlo usando Access. Ejercicio 5. Usar la base de datos Empresa. Realizar una aplicación que manejando ADO Conectado inserte y modifique empleados desde una ficha con textbox, en lugar de un DataGridView. Se incorporará un DataGridView donde se mostrarán todos los empleados de la base de datos Empresa, a petición del usuario, por mediación de un botón llamado CargarDatos. Ejercicio 6. Tomando como base el ejercicio 5, desarrollar una aplicación que permita dar de alta los empleados, pero en lugar de introducir el Departamento y la Categoría con un número, que su valor correspondiente sea seleccionado desde un ComboBox. Los ComboBox mostrarán el nombre del departamento o la categoría según corresponda. Además, desde la aplicación, podremos dar altas, modificaciones y eliminación de categorías y departamentos. Hay que tener en cuenta que si se borra una categoría o departamento que están dados de alta en la tabla empleados, el SQL Server dará un error. Ejercicio 7. Realizar una aplicación que permita mostrar en un DataGridView la información de la tabla empleados, seleccionando las columnas que queremos ver desde un panel con CheckBox. Cada CheckBox es una de las columnas de la tabla. Mostraremos las columnas que su CheckBox este activado. Ejercicio 8. Realizar una aplicación que tomando como base la base de datos Librería, nos muestre en un DataGridView los libros comprados por cada cliente con toda la información del libro. Además y en otro DataGridView mostrará toda la información de los libros que componen un tema. Tanto para el caso de clientes, como para el caso de temas, se introducirá el Id correspondiente seleccionado desde un ComboBox.

Capítulo 4. Programación en ADO.

Página 68

Sign up to vote on this title
UsefulNot useful