Está en la página 1de 30

Pasar de ADO a ADO.NET

9. Al pulsar el botón Finalizar se generará el juego de datos tipificado Northwind- DataSet y se mostrará en el panel Orígenes de datos. Expanda la rama Customers para mostrar las columnas de la tabla del mismo nombre tal como se muestra en la siguiente figura.

del mismo nombre tal como se muestra en la siguiente figura. El nuevo objeto SqlConnection creado

El nuevo objeto SqlConnection creado en los pasos 3 a 5 aparece en el nodo Conexiones de datos del Explorador de servidores como servidor\sqlexpress.Northwind.dbo. El nodo se puede renombrar en el Explorador de servidores con un nombre más sencillo, por ejem- plo localhost.Northwind; el cambio no afecta a los objetos dependientes del proyecto.

Añadir un DataSet tipificado genera un esquema XSD, NorthwindDataSet.xsd en este ejemplo, y añade 1.197 líneas de código VB 2005 al archivo de clase parcial North- windDataSet.Designer.vb, cuyo tamaño es de 73 KBytes. Las clases parciales son una característica nueva de VB 2005 y C# que permite expandir una clase, como la North- windDataSet, con archivos de clase adicionales. VB 2005 usa la sentencia Public Partial Class className para identificar archivos de clase parcial. Deberá tener seleccionado el botón Mostrar todos los archivos del panel Explorador de soluciones para ver Northwind- DataSet.Designer.vb y los dos archivos vacíos NorthwindDataSet.xsc y Northwind- DataSet.xss.

Realice una doble pulsación en el nodo NorthwindDataSet.xsd situado en el Explorador de proyectos para mostrar la tabla de datos Customers y su adaptador de tabla Customers asociado, tal como muestra la siguiente figura, en la ventana principal. El código VB 2005 en DataSetName.Designer.vb proporciona IntelliSense para los objetos DataSet y los objetos, anteriormente vinculados, DataTable y DataSet. El código también permite acceso directo a las clases nombradas, métodos y eventos para DataSet y su adaptador de tabla del NorthwindDataSet.Designer.vb, código de las listas Classes y métodos de la ventana.

Bases de datos con Visual Basic

Si ya ha trabajado con DataSets tipificados en VS 2003, se dará cuenta cuenta de que el esquema para ADO 2.0 DataSets tiene mucha más verborrea que la versión ADO 1.x, que consta sólo de 30 líneas que definen el Customers DataSet. ADO.NET 2.0 prefija el esquema de tiempo de diseño con 258 líneas de información <xs:annotation>, que proporcionan una definición completa del DataSet y su string de conexión, comandos y parámetros, así como los datos del mapping de columnas. La parte del esquema que define los elementos para los campos de tabla crece de 30 a 94 líneas porque las definiciones de los elementos contienen ahora valores para el atributo maxLength y utilizan atributos restrictionBase para especifi- car los tipos de dato XSD

restrictionBase para especifi- car los tipos de dato XSD En la siguiente figura puede ver Internet

En la siguiente figura puede ver Internet Explorer mostrando las primeras líneas de las 352 que componen el esquema.

En la siguiente figura puede ver Internet Explorer mostrando las primeras líneas de las 352 que

Pasar de ADO a ADO.NET

Private Sub bindingNavigatorSaveItem_Click(ByVal sender As System.Object, ByVal e As System.EventArgs) Handles bindingNavigatorSaveItem.Click Me.CustomersBindingSource.EndEdit() Me.CustomersTableAdapter.Update(Me.NorthwindDataSet.Customers) End Sub

La siguiente figura muestra el formulario final después de reducir el tamaño, ampliar el control de DataGridView para llenar el espacio disponible y pulsar <F5> para crear, depurar y ejecutar el proyecto.

<F5> para crear, depurar y ejecutar el proyecto. La CustomersDataGridView está vinculada a la tabla

La CustomersDataGridView está vinculada a la tabla Customers y se puede editar por defecto. Los cambios que se hagan en la DataGridView no se validan en la tabla hasta que no se pulsa el botón Save Data.

Para facilitar la edición, el ancho de columna se puede adaptar automáticamente al con- tenido definiendo para la propiedad AutoSizeColumnsMode de DataGridView el valor AllCells o DisplayedCells, que añade una barra de desplazamiento horizontal al control.

1.9 Persistir y reabrir el juego de datos

El manejador de eventos del proyecto frmDataGridView_Load incluye el siguiente códi- go para salvar el documento de datos XML NorthwindDataSet y el esquema solo. Se puede añadir código parecido después de la última invocación DataComponent.Fill o DataAdapter.Fill de cualquier proyecto para persistir su juego de datos.

Private Sub Form1_Load(ByVal sender As System.Object, ¨ ByVal e As System.EventArgs) Handles MyBase.Load 'TODO: This line of code loads data into the 'NorthwindDataSet.Customers' table. 'You can move, or remove it, as needed. Me.CustomersTableAdapter.Fill(Me.NorthwindDataSet.Customers) Dim strPath As String = Application.StartupPath With Me.NorthwindDataSet .WriteXml(strPath + "CustsNoSchema.xml", XmlWriteMode.IgnoreSchema)

31

Bases de datos con Visual Basic

.WriteXml(strPath + "CustsWithSchema.xml", XmlWriteMode.WriteSchema) .WriteXmlSchema(strPath + "CustsSchema.xsd") End With End Sub

Persistiendo el DataSet como documento XML, sin el esquema incrustado, permite dar soporte a los usuarios sin conexión, cargando de nuevo el DataSet del archivo. La sen- tencia siguiente se puede substituir por Me.CustomersTableAdapter.Fill(Me.North- windDataSet.Customers) cuando el usuario está desconectado:

Me.NorthwindDataSet.ReadXml(strPath + CustsNoSchema.xml , XmlReadMode.Auto)

El escenario en el mundo real para persistir y cargar de nuevo un juego de datos es mucho más complejo que lo que hemos visto aquí. En capítulos posteriores se describe cómo salvar y cargar de nuevo los cambios pendientes del DataSet que no se han pasado a las tablas base. El argumento XmlReadMode.Auto aparece por defecto, así que incluirlo es opcional.

1.10 Cambiar de un DataViewGrid a un Details Form

La combinación por defecto de los controles DataViewGrid y DataNavigator acelera la creación de un formulario utilizable. De todos modos, un DataNavigator es mucho más útil para crear un formulario de detalles que muestre en pantalla los valores de colum- na en cuadros de texto u otros controles vinculados, como selectores de datos DateTime y cuadros de verificación para valores booleanos.

La ventana Data Sources facilita el cambio de la DataGridView a un formulario de deta- lle. Borre el control DataGridView, muestre la ventana Orígenes de datos, abra la lista des- plegable para la tabla de datos, y seleccione Detalles como se muestra en la siguiente figura.

Detalles como se muestra en la siguiente figura. Arrastre el icono DataTable hasta el formulario para

Arrastre el icono DataTable hasta el formulario para añadir automáticamente una columna de etiquetas con controles asociados de vinculación de datos (cuadros de texto en este ejemplo) al formualrio. La siguiente figura, que es una versión modificada del proyecto GeneratedDataGridView, muestra las etiquetas y los cuadros de texto reordena-

32

Pasar de ADO a ADO.NET

dos para reducir la altura del formulario.

de ADO a ADO.NET dos para reducir la altura del formulario. 1.11 Añadir un control de

1.11 Añadir un control de vínculo de datos relacionado

Al panel Orígenes de datos se le puede añadir una tabla relacionada y después un con- trol, como DataGridView, que se puede vincular al BindingAdapter relacionado. Para añadir un control relacionado OrdersDataGridView a una copia del proyecto Genera- tedDetailView.sln, Debe realiar los siguientes pasos:

1. Copie y pege la carpeta GeneratedDetailView y renombre la nueva carpeta como OrdersDetailView. No renombre el proyecto.

2. Pulse <F5> para crear y compilar el proyecto. Corrija cualquier error de nombre que detecte el depurador.

3. Abra la ventana Orígenes de datos y pulse el botón del ayudante Configurar Dataset con el asistente para abrir la página Elija los objetos de la base de datos.

4. Expandir el árbol Tablas y seleccione la casilla de verificación de la tabla Orders. Pulse el botón Finalizar. De ese modo se añade en panel Orígenes de datos un nodo relacional Orders a la tabla Customers y un nodo individual Orders (ver siguiente figura).

5. Con DataGridView seleccionado en la lista desplegable, arrastre el nodo Orders rela- cionado por debajo de los cuadros de texto vinculados del formulario para autoge- nerar un control OrdersDataGridView.

6. Ajuste el tamaño y la posición de los controles y defina para la propiedad Or- dersDataGridView.AutoSizeRowsMode el valor DisplayedCells. Opcionalmente se puede modificar la propiedad Text del formulario para reflejar el cambio en el diseño.

33

Bases de datos con Visual Basic

Bases de datos con Visual Basic 7. Pulse <F5> para crear y ejecutar el proyecto. El

7.

Pulse <F5> para crear y ejecutar el proyecto. El formulario aparecerá tal como muestra la siguiente figura.

formulario aparecerá tal como muestra la siguiente figura. Arrastrando el nodo relacionado de la tabla Orders

Arrastrando el nodo relacionado de la tabla Orders hasta el formulario se añade un OrdersTableAdapter y OrdersBindingSource a la bandeja, y el control OrdersDataGridView al formulario. El valor de la propiedad OrdersDataGridView del control DataSource es OrdersBindingSource.

34

Pasar de ADO a ADO.NET

La propiedad OrdersBindingSource tiene el valor CustomersBindingSource y el valor de la propiedad DataMember es FK_Orders_Customers, el cual es la relación de clave foránea en el campo CustomerID entre las tablas de Customers y Orders. Para verificar las pro- piedades de FK_Orders_Customers debe abrir el NorthwindDataSet.xsd en la ventana principal, pulsar con el botón secundario la línea de relación entre las tablas Customers y Orders, y seleccionar Editar relación para abrir el cuadro de diálogo Relación (ver figu- ra siguiente).

el cuadro de diálogo Relación (ver figu- ra siguiente). Las relaciones que se definen añadiendo tablas

Las relaciones que se definen añadiendo tablas relacionadas a la ventana Orígenes de datos no refuerzan la integridad referencial por defecto. Hay que cambiar el valor por defecto de la propiedad Sólo relación a uno de los otros valores para mantener la integridad referen- cial. También se puede especificar Cascade u otras opciones para las reglas actualización, eliminación, y aceptación o rechazo.

35

CAPÍTULO 2

Las novedades de ADO.NET 2.0

En este capítulo trataremos los nuevos objetos de ADO.NET 2.0 y los métodos, propie- dades y eventos utilizados con ellos. De la misma forma que el capítulo anterior, este capítulo empieza con unas descripciones de los nuevos objetos en tiempo de ejecución, como DbProviderFactory y SqlBulkCopy, con los correspondientes ejemplos de código para crear y manejar los nuevos objetos. El capítulo continúa com más ejemplos avan- zados de las componentes y controles de ADO.NET 2.0 para los formularios de Windows, que se pueden agragar con la ayuda de diseñadores: DataTables, BindingSour- ces, BindingNavigators y DataGridViews.

Todos los ejemplos de código SQLServer de este capítulo se pueden ejecutar con SQL- Server 2000, SQLServer 2005 o SQLServer 2005 Express Edition (SQLX) y nencesitan los privilegios del administrador del sistema.

Si trabajamos con SQLX, deberemos cambiar la cadena de conexión de cada proyecto de local- host a .\SQLEXPRESS.

2.1 Los objetos de formulario

Este libro define un objeto en tiempo de ejecución como un tipo de objeto no visual, relacionado con los datos que se genera sin la ayuda de los múltiples asistentes. Los objetos en tiempo de ejecución de ADO.NET 2.0 se crean escribiendo código VB.NET 2005 sin la ayuda de los ayudantes de tiempo-diseño de VS 2005 ni código autogenera- do. Microsoft ha dedicado una parte importante del esfuerzo invertido en el desarrollo de VS 2005 y ADO.NET 2.0 en simplificar con arrastrar y colocar la creación de formu- larios básicos Windows y Web de vinculación de datos.

Otro aspecto que se ha cuidado ha sido dar soporte a las nuevas características del SQLServer 2005 con los objetos System.Data y System.Xml. Por eso, ADO.NET 2.0 sólo incluye algunos objetos y características nuevas y actualizadas que son compatibles con las fuentes de datos de SQLServer 2000. Más adelante, en este mismo libro, se tratarán las propiedades de ADO.NET 2.0 y VB.NET 2005 específicas para SQLServer 2005.

A

continuación indicamos los objetos en tiempo de ejecución y actualizados, métodos

y

características de lenguaje, más importantes para los proyectos de formulario

Windows:

37

Bases de datos con Visual Basic

El objeto DbProviderFactory permite escribir código común para proveedores de datos y servidores de bases de datos alternativos.

El objeto SqlBulkCopy permite insertar con gran eficiencia datos de SQLServer de fuentes relacionales y XML.

El método SqlConnection.RetrieveStatistics proporciona información detallada sobre la conexión abierta con el SQLServer.

La ejecución asincrónica de SqlCommand permite entrelazar consultas o actualiza- ciones múltiples de larga ejecución.

Los nuevos objetos actualizados DataTable soportan las características comunes de los DataSet, como son los métodos ReadXml y WriteXml, retornan valores de los ser- vicios Web e interfaces remotas y streaming.

A las tablas de datos se les puede asignar espacios-nombre y prefijos para los nom- bre de espacio.

Los tipos Null permiten definir objetos fuertemente tipificados, con miembros a los que se puede asignar el valor DbNull.

En las secciones siguientes se explica cómo utilizar las características del precedente ADO.NET 2.0 con ejemplos de código derivado de los proyectos-ejemplo de formula- rios Windows.

2.1.1 Utilizar DbProviderFactories para crear proyectos con bases de datos agnósticas

La nueva clase System.Data.Common.DbProviderFactories proporciona a los desarrolla- dores de bases de datos la oportunidad de enfrentarse a la creación de aplicaciones agnósticas frente a las fuentes de datos. Crear aplicaciones de entradas de datos no-tri- viales que puedan interactuar sin fisuras con todos los administradores de bases de datos relacionales, para los que existen proveedores de datos controlados, no es preci- samente algo simple. Las diferencias menores en la sintaxis SQL, tipos de datos, dialec- tos de procedimientos almacenados, tratamiento de error, y otras características pro- pias de una base de datos, requerirán sin duda un esfuerzo. Si actualmente utiliza el proveedor de datos controlados .NET Framework OleDb, o ADODB con proveedores OLE DB para asegurar la interoperabilidad de las bases de datos, seguramente encon- trará que Microsoft y el tercero en cuestión, ADO.NET , ofrecen mejor rendimiento y, como resultado, mayor escalabilidad. Por otra parte, el nivel de ampliación y mejora que .NET garantiza a los proveedores de datos, hace difícil escribir código que sea total- mente transparente al proveedor.

Los grupos de terceros de proveedores controlados .NET pueden reducir la interoperabilidad con costes de licencia añadidos. Por ejemplo, DataDirect Technologies ofrece proveedores de datos controlados para IBM DB2 y DB2 UDB; Oracle 8i, 9i, y 10g; SQLServer 7 y 2000; Sybase Adaptive Server 11.5 y 11.9; y Sybase Adaptive Server Enterprise 12.0 y 12.5. Todos los prove- edores DataDirect buscan salidas para minimizar las diferencias de sintaxis SQL y comunicar- se con servidores a través de los protocolos de los vendedores de bases de datos.

38

Las novedades de ADO.NET 2.0

Crear un objeto DataReader de la clase DbProviderFactories es un proceso en siete pasos:

1. Crear un objeto DbProviderFactory pasando el nombre completo de la clase del pro- veedor de datos, como System.Data.SqlClient, al argumento de una sentencia DimFactoryNameAsDbProviderFactory = DbProviderFactories.GetFactory(strProvider).

2. Crear un objeto IdbConnection invocando el método DimConnectionNameAsIDbCon- nection = FactoryName.CreateConnection().

3. Definir el valor de la propiedad ConnectionName.Connection.String.

4. Crear un objeto IdbCommand invocando el método DimCommandNameAsIDbCom- mand = ConnectionName.CreateCommand().

5. Definir para las propiedades CommandName.CommandType (opcional) y Command- Name.CommandText los valores adecuados para el proveedor.

6. Llamar al método ConnectionName.Open().

7. Crear un objeto IdataReader invocando el método DimReaderNameAsIDataReader = CommandName.ExecuteReader.

El objeto IDataReader tiene los miembros que los DataReaders específicos del proveedor para ADO.NET 1.x y 2.0, más el nuevo método GetSchemaTable que se describe en el si- guiente apartado.

El proyecto de ejemplo DbFactoryTest.sln muestra datos en pantalla de una de las tres tablas Northwind creando y atravesando los objetos IDataReader de SqlClient, OleDb, u Odbc, que se especifiquen seleccionando la opción apropiada. El formulario incluye también un control DataGridView con el que se muestra en pantalla el esquema de tabla DataTable (del que trata el siguiente apartado) tal como muestra la siguiente figura.

el siguiente apartado) tal como muestra la siguiente figura. El siguiente listado contiene el código para

El siguiente listado contiene el código para las declaraciones de variables y el botón de opción del manejador de eventos de OleDb DbProviderFactory:

39

Bases de datos con Visual Basic

'OleDb provider settings - Products table Private strOleDbProvider As String = "System.Data.OleDb" Private strOleDbConn As String = "Provider=SQLOLEDB;Data Source=.\SQLEXPRESS;" + _ "Initial Catalog=Northwind;Integrated Security=SSPI" Private strOleDbTable As String = "Products"

Private Sub optOleDb_CheckedChanged(ByVal sender As System.Object, _ ByVal e As System.EventArgs) Handles optOleDb.CheckedChanged If optOleDb.Checked = True Then PopulateList(strOleDbProvider, strOleDbConn, strOleDbTable) Me.Text = "DbFactory Test Form - OleDb" End If End Sub El tratador de eventos optOleDB_CheckedChanged pasa los valores requeridos del parámetro OleDb al procedimiento PopulateList, ampliado con el código siguiente:

Private Sub PopulateList(ByVal strProvider As String, _ ByVal strConn As String, ByVal strTable As String) Dim cnFactory As IDbConnection = Nothing Dim drData As IDataReader = Nothing Try

Dim dpFactory As DbProviderFactory = _ DbProviderFactories.GetFactory(strProvider) cnFactory = dpFactory.CreateConnection() cnFactory.ConnectionString = strConn Dim cmFactory As IDbCommand = cnFactory.CreateCommand cmFactory.CommandType = CommandType.Text cmFactory.CommandText = "SELECT * FROM " + strTable cnFactory.Open() drData = cmFactory.ExecuteReader(CommandBehavior.KeyInfo) lstData.Items.Clear() Dim dtSchema As DataTable With drData While drData.Read lstData.Items.Add(.GetValue(0).ToString + _ " - " + .GetValue(1).ToString) End While dtSchema = drData.GetSchemaTable() With dgvSchema If dtSchema.Columns.Count > 1 Then .RowHeadersVisible = False .DataSource = dtSchema .AutoGenerateColumns = True Application.DoEvents() If .Columns.Count > 0 Then .Columns(0).Frozen = True .Columns("BaseSchemaName").Width = 110 If .Columns.Count = 24 Then .Columns(23).Width = 200 End If End If

40

Las novedades de ADO.NET 2.0

End If End With End With If dgvSchema.Columns.Count > 0 Then Dim intCtr As Integer Dim strDataCols As String = "" For intCtr = 0 To dgvSchema.Rows(0).Cells.Count - 1 strDataCols += dgvSchema.Columns(intCtr).Name + vbTab + _ dgvSchema.Rows(0).Cells(intCtr).Value.ToString + vbCrLf Next intCtr intCtr = 0 End If Catch exc As Exception MsgBox(exc.Message + exc.StackTrace) Finally If Not drData Is Nothing Then drData.Close() End If If Not cnFactory Is Nothing Then cnFactory.Close() End If End Try End Sub

Hay que especificar CommandBehavior.KeyInfo como el argumento ExecuteReader para devol- ver las claves primarias correctas y las propiedades de campo relacionadas.

Si sus proyectos deben incluir independencia respecto al proveedor de datos y está dis- puesto a escribir más para especificar las diferencias, sutiles o no, entre las diferentes mejoras de los proveedores de datos, pruebe con DbProviderFactories. Sin embargo, tenga en cuenta que el código independiente de proveedores tiene que usar tipos de datos originales .NET, antes que los tipos de datos específicos de cada proveedor para los diferentes add-in de SQLServer, Oracle, y otros servidores soportados por terceros.

DbProviderFactories mejora la vinculación de la base de datos, lo que deja en clara desventa- ja a muchas propiedades del modelo de programación de ADO.NET. El SQL específico de un vendedor y la sintaxis de ejecución de los procedimientos almacenados hacen que escribir código transparente al vendedor con los proveedores de datos ADO.NET 2.0 sea difícil, si no imposible.

2.1.2 Restablecer los esquemas de las tablas base

Los DataReaders de ADO.NET 1.x y 2.0 y los DataTableReaders de ADO.NET 2.0, tienen un método GetSchemaTable que devuelve los correspondientes esquemas del objeto en un objeto DataTable. Para dar información sobre el tipo de datos utilizado en los proyec- tos, que substituye el código para los controles de vinculación que muestran y actuali-

41

Bases de datos con Visual Basic

zan las tablas base, se utilizan los valores de propiedad del esquema DataTable. Los esquemas DataTable dan valores ColumnLength para definir la propiedad MaxLength en cuadros de texto y valores IsReadOnly que se pueden aplicar a la propiedad ReadOnly de los controles de entrada de datos normal. Estos DataTable también devuelven infor- mación clave primaria como son los índices de columna y detalles de autoincremen- tación.

El

System.Data.ObjectSpaces.ObjectDataReader, que se incluía en las primeras versiones alpha

y

Community Technical Preview de VS 2005, daban miembros similares a los de otros

DataReaders, incluido el método GetSchemaTable. En Mayo del 2004, Microsoft anunció que ObjectSpaces se lanzaría como componente dentro de las mejoras del sistema de archivos WinFS.

Para crear un esquema DataTable en un DataReader y poblar una DataGridView para mostrar las propiedades de columna se ha de utilizar código parecido al siguiente:

Dim dtSchema As DataTable With drData While drData.Read lstData.Items.Add(.GetValue(0).ToString + _ " - " + .GetValue(1).ToString) End While dtSchema = drData.GetSchemaTable() With dgvSchema If dtSchema.Columns.Count > 1 Then .RowHeadersVisible = False .DataSource = dtSchema .AutoGenerateColumns = True Application.DoEvents() If .Columns.Count > 0 Then .Columns(0).Frozen = True .Columns("BaseSchemaName").Width = 110 If .Columns.Count = 24 Then .Columns(23).Width = 200 End If End If End If End With End With

El esquema DataTable contiene una fila por cada columna de tabla base y 27 campos de propiedades de columna SqlDataReader. OleDbDataReaders y OdbcDataReaders devuel- ven 18 propiedades; los DataTableReaders tienen 25 campos de propiedades. Como el objeto DataTableReader es nuevo en ADO.NET 2.0, en la tabla siguiente se comparan el índice de campos del esquema DataTable y los nombres de propiedades de las tres cla- ses de DataReaders.

42

Las novedades de ADO.NET 2.0

Index

SqlDataReader

OleDb y Odbc DataReaders

DataTableReader

0

ColumnName

ColumnName

ColumnName

1

ColumnOrdinal

ColumnOrdinal

ColumnOrdinal

2

ColumnSize

ColumnSize

ColumnSize

3

NumericPrecision

NumericPrecision

NumericPrecision

4

NumericScale

NumericScale

NumericScale

5

IsUnique

DataType

DataType

6

IsKey

ProviderType

ProviderType

7

BaseServerName

IsLong

IsLong

8

BaseCatalogName

AllowDBNull

AllowDBNull

9

BaseColumnName

IsReadOnly

IsReadOnly

10

BaseSchemaName

IsRowVersion

IsRowVersion

11

BaseTableName

IsUnique

IsUnique

12

DataType

IsKey

IsKey

13

AllowDBNull

IsAutoIncrement

IsAutoIncrement

14

ProviderType

BaseSchemaName

BaseCatalogName

15

IsAliased

BaseCatalogName

BaseSchemaName

16

IsExpression

BaseTableName

BaseTableName

17

IsIdentity

BaseColumnName

BaseColumnName

18

IsAutoIncrement

 

AutoIncrementSeed

19

IsRowVersion

 

AutoIncrementStep

20

IsHidden

 

DefaultValue

21

IsLong

 

Expression

22

IsReadOnly

 

ColumnMapping

23

ProviderSpecificDataType

 

BaseTableNamespace

24

DataTypeName

 

BaseColumnNamespace

25

XmlSchema Collection Database

   

26

XmlSchema Collection OwningSchema

   

27

XmlSchema CollectionName

   

Las propiedades que se muestran en negrita son miembros de la nueva clase de ADO.NET 2.0 System.Data.Common .SchemaTableColumn y son necesarias. El resto son miembros opciona- les de la clase SystemData.Common.SchemaOptionalTableColumn. Los campos XmlSchema- Collection aparecen sólo en las tablas del SQLServer 2005 y especifican el esquema, si existe, de los campos para los tipos de datos xml.

Los desarrolladores de bases de datos pueden traducir la mayor parte de las propieda- des incluidas en la tabla-lista anterior. Por eso la tabla siguiente sólo ofrece la lista de las propiedades cuyo significado no es obvio o que devuelven valores inesperados.

43

Bases de datos con Visual Basic

Nombre de la propiedad

Descripción

ColumnSize

Devuelve –1 si el dato no está disponible, de lo contrario, el tamaño de la columna en bytes.

DataType

El tipo de datos original de .NET que corresponde al tipo de dato de la columna, como en System.Int32 o System.String.

ProviderType

El valor íntegro de una enumeración de tipo de datos especifícos del proveedor.

IsLong

True indica un tipo de datos text o ntext de SQL, o un image,y un campo de objeto OLE o Jet Memo.

ProviderSpecificDataType

Uno de los tipos Sql, como SqlString o SqlInt32 (sólo SqlClient)

Expression

La expresión calculada para una columna de una DataTable (sólo DataTable)

ColumnMapping

Un valor String que especifica la columna de la tabla de destino o 1 si la columna no está mapeada (sólo DataTable)

BaseTableNamespace

El nombre de espacio XML asignado a la tabla, heredado del nombre de espacio del DataSet si está vacío (sólo DataTable)

BaseColumnNamespace

El nombre de espacio XML asignado a la tabla, heredado del nombre de espacio del DataSet si está vacío (sólo DataTable)

XmlSchema Collection Database

El nombre de la base de datos del servidor SQL Server 2005 que contiene el conjunto de esquemas para una columna del tipo xml (null si la columna xml no tiene esquema)

XmlSchema CollectionOwning Schema

Esquema relacional del SQL Server 2005 que contiene el conjun- to de XmlSchema (null si la columna xml no tiene esquema)

XmlSchema CollectionName

Nombre del conjunto de esquemas para una columna del tipo xml (null si la columna xml no tiene esquema)

Más adelante en este capítulo, se describe cómo cargar y persistir DataTables desde bases de datos y archivos XML, y mostrar en pantalla la información del esquema de los objetos DataTable.

2.2 Comprobar las instancias de servidor SQL disponibles y los proveedores de datos ADO.NET 2.0

El método System.Data.Common.SqlDataSourceEnumerator.Instance.GetDataSources de- vuelve una DataTable que tiene una fila para cada instancia de servidor SQL 2000 y 2005 accesibles. Las columnas muestran las propiedades ServerName, InstanceName, IsClustered, y Version.

Al invocar el método DbProviderFactories.GetFactoryClasses(), éste devuelve una tabla similar con una fila para cada proveedor Microsoft de datos controlados .NET instala-

44

Las novedades de ADO.NET 2.0

dos en el sistema, con columnas para las propiedades del proveedor Name, Description, InvariantName, y AssemblyQualifiedName y el número de SupportedClasses. Los provee- dores de datos a terceros, como Oracle ODP.NET con Oracle 10g (Oracle.DataAccess.dll), no aparecen en la tabla.

El archivo machine.config contiene un elemento para cada uno de los cuatro espacios de nom- bre de proveedores de datos ADO.NET 2.0, y una sección system.data que añade estos provee- dores a DbProviderFactories. El método GetFactoryClasses lee el archivo machine.config para proporcionar la lista de proveedores instalados.

El siguiente código, del proyecto de ejemplo DataEnums.sln, puebla dos controles Data- GridView con una instancia de SQLServer y un proveedor instalado de datos .NET de Microsoft:

Private Sub frmDataEnums_Load(ByVal sender As System.Object, _ ByVal e As System.EventArgs) Handles MyBase.Load Dim dtServers As DataTable = SqlDataSourceEnumerator.Instance.GetDataSources With dgvServers .DataSource = dtServers .AutoGenerateColumns = True .RowHeadersVisible = False .BorderStyle = BorderStyle.None End With Dim dtProviders As DataTable = DbProviderFactories.GetFactoryClasses() With dgvProviders .DataSource = dtProviders .AutoGenerateColumns = True .RowHeadersVisible = False .RowTemplate.Height = 22 .BorderStyle = BorderStyle.None End With End Sub

Al ejecutar el proyecto DataEnums, éste enumera las instancias de SQLServer y los pro- veedores de datos instalados. La Figura siguiente muestra una instancia por defecto de un SQLServer 2000 (OAKLEAF-W2K3), y una instancia MSDE con nombre (OAKLEAF- W2K3\SHAREPOINT), una instancia de SQLServer 2005 (OAKLEAF-MS18), y una ins- tancia SQLExpress (SQLX) con nombre (OAKLEAF-MS18\SQLEXPRESS), así como pro- veedores de datos accesibles o instalados en el ordenador de desarrollo utilizado para escribir este libro.

45

Bases de datos con Visual Basic

Bases de datos con Visual Basic 2.2.1 Entradas Batch en tablas de servidor SQL con el

2.2.1 Entradas Batch en tablas de servidor SQL con el objeto SqlBulkCopy

La utilidad BCP del SQLServer y la sentencia BULK INSERT son los métodos tradicio- nales para añadir filas muy rápidamente a las tablas del SQL Server. ADO.NET 2.0 ofre- ce una opción alternativa: programar el nuevo objeto SqlBulkCopy. La fuente más habi- tual para las filas son los DataReader en tablas relacionales. Otra alternativa es insertar filas desde documentos tabulares XML creando un juego de datos en tiempo de ejecu- ción runtime con una o más tablas de datos para copiar.

Copiar documentos XML a las tablas del servidor SQL (un proceso llamado shredding) es mucho más sencillo con SqlBulkCopy que con la propiedad para cargar de SQLXML3.0. Cargar requiere un esquema XML anotado para mapear elementos o atributos y añadirlos a las colum- nas de las tablas base. SqlBulkCopy tiene una colección de ColumnMappings que permite defi- nir la relación entre las columnas de la tabla de datos fuente y las de la tabla base destino.

Para insertar filas de un DataReader en una tabla base destino ya existente, hay que:

1.

Crear una conexión y un comando para los datos fuente. Se puede usar cualquier proveedor .NET para conectarse a la fuente de datos y crear el DataReader.

2.

Aplicar el método Command.ExecuteReader para crear el DataReader.

3.

Crear un objeto nuevo SqlBulkCopy que tendrá como argumentos el string de cone- xión y la enumeración apropiada en SqlBulkCopyOptions.

4.

Definir el valor de la propiedad SqlBulkCopy.DestinationTableName.

5.

Añadir miembros ColumnMapping a la colección ColumnMappings si el esquema de la tabla destino difiere de la tabla o la petición fuente.

6.

Definir otros valores opcionales para la propiedad SqlBulkCopy, como BatchSize y BulkCopyTimeout.

46

Las novedades de ADO.NET 2.0

7. Si la operación de copia implica un número muy alto de registros o ejecuciones con una conexión de red muy lenta, añadir un tratador para el evento SqlBulkCopy.Sql- RowsCopied a fin de mostrar en pantalla el número o el porcentaje de registros copiados.

8. Invocar el método SqlBulkCopy.WriteToServer para ejecutar la operación de copia.

9. Aplicar el método SqlBulkCopy.Close() y, si ya ha terminado, cierre la conexión. En caso contrario, use de nuevo el objeto SqlBulkCopy para realizar cualquier otra ope- ración.

La tabla siguiente describe los miembros de la enumeración SqlBulkCopyOptions.

Nombre del miembro

Descripción

CheckConstraints

Aplica un chequeo restringido durante el proceso de copia.

Default

No utiliza opciones (por defecto) para la operación de copiar.

FireTriggers

Permite a detonadores INSERT dispararse durante el proceso de copia.

KeepIdentity

Utiliza valores de identificación de la tabla fuente en lugar de generar nuevos valores de identidad basados en los valores de integridad e incremento de la tabla destino.

KeepNulls

Conserva los valores null de la tabla fuente a pesar de los valores por defecto de las tablas destino.

TableLock

Aplica un candado a toda la tabla durante el proceso de copia, en lugar del candado por defeccto aplicado por filas.

UseInternalTransaction

Hace que cada batch de la copia bulk se ejecute dentro de una tran- sacción.

KeepIdentity es el miembro más importante de la enumeración SqlBulkCopyOptions para tablas que usan una columna de identificación como clave primaria. Si no se especifica esta opción, las claves de la tabla destino podrían ser distintas de los valores en la tabla fuente. También es con- veniente añadir la opción UseInternalTransaction para prevenir copias parciales si ocurriera alguna excepción durante el proceso.

El ejemplo más sencillo de una operación SqlBulkCopy crea copias de tablas en la misma base de datos. El siguiente código del proyecto BulkCopySameSchema.sln copia las tablas de productos Northwind (Northwind Products) como ProductsCopy:

Private Sub btnCopyProds_Click(ByVal sender As System.Object, ByVal e As System.EventArgs) Handles btnCopyProds.Click Dim sdrProds As SqlDataReader = Nothing Dim sbcProds As SqlBulkCopy = Nothing Try

Dim lngTime As Long = Now.Ticks btnCopyProds.Enabled = False cnnNwind.Open() cmdProds.CommandText = "DELETE FROM ProductsCopy" Dim intRecs As Integer = cmdProds.ExecuteNonQuery

47

Bases de datos con Visual Basic

cmdProds.CommandText = "SELECT * FROM Products" sdrProds = cmdProds.ExecuteReader() If chkKeepIdentity.Checked Then sbcProds = New SqlBulkCopy(strConn, _ SqlBulkCopyOptions.UseInternalTransaction Or _ SqlBulkCopyOptions.KeepIdentity)

Else

sbcProds = New SqlBulkCopy(strConn, _ SqlBulkCopyOptions.UseInternalTransaction) Dim blnUseCm As Boolean = True If blnUseCm Then sbcProds.ColumnMappings.Clear() Dim intCol As Integer For intCol = 1 To 9 sbcProds.ColumnMappings.Add(intCol, intCol) Next intCol End If End If AddHandler sbcProds.SqlRowsCopied, New SqlRowsCopiedEventHandler(AddressOf ProdRowAdded)

With sbcProds .DestinationTableName = "ProductsCopy" .BatchSize = CInt(nudBatchSize.Value) .BulkCopyTimeout = 30 .NotifyAfter = 1 .WriteToServer(sdrProds) .Close() End With sdrProds.Close() lngTime = Now.Ticks - lngTime txtTime.Text = Format(lngTime / 10000000, "0.000") FillProdsList(True) Catch excCopy As Exception MsgBox(excCopy.Message + excCopy.StackTrace, , "Products Bulk Copy Exception")

Finally If Not sbcProds Is Nothing Then sbcProds.Close() End If If Not sdrProds Is Nothing Then sdrProds.Close() End If If Not cnnNwind Is Nothing Then cnnNwind.Close() End If btnCopyProds.Enabled = True End Try End Sub

48

Las novedades de ADO.NET 2.0

La propiedad SqlBulkCopy.NotifyAfter determina el número de filas añadidas antes de dispararse el evento SqlRowsCopied. A continuación vemos el código para un tratador de eventos SqlRowsCopied que muestra el progreso del proceso de copia de las tablas de productos en un cuadro de texto:

Sub ProdRowAdded(ByVal oSource As Object, ByVal oArgs As SqlRowsCopiedEventArgs) txtProdRows.Text = oArgs.RowsCopied.ToString Application.DoEvents() End Sub

Mostrar el progreso de la copia reduce sustancialmente la velocidad de la copia. En las aplica- ciones finales que deben proporcionar interacción al usuario, el valor de la propiedad NotifyAfter debe ser como mínimo el 10 por ciento del número total de registros añadidos.

La siguiente figura muestra el formulario del proyecto BulkCopySameSchema.sln des- pués de copiar las dos tablas. Los scripts Transact-SQL recrean la tabla en el manejador de eventos frmBulkCopy_Load. Los cuadros de lista muestran la clave primera de la tabla fuente y los valores de segunda columna cuando se carga el formulario, y los valores de la tabla destino después de la copia. El deslizador Batch Size determina el número de filas por intervalo; 0 (el valor por defecto) intenta enviar todas las filas al servidor en un solo intervalo. Definiendo 1 para el tamaño del intervalo y copiando de nuevo las tablas se puede comparar el rendimiento de la copia frente a las operaciones fila por fila.

de la copia frente a las operaciones fila por fila. Cachear datos y código provoca una

Cachear datos y código provoca una diferencia considerable entre el tiempo de ejecución de la copia bulk inicial y las siguientes. Por lo tanto, habría que comparar los tiempos de ejecución con batchs de diferentes tamaños después de una o dos pruebas con un tamaño de batch defini- do en 0.

Deseleccionando el cuadro de verificación Keep Source Identity, la opción KeepIdentity se elimina del constructor SqlBulkCopy de la tabla de productos. En este caso, los valores de clave primarios se incrementan en 77 por cada operación de copia. En el apartado

49

Bases de datos con Visual Basic

siguiente se describe el tratador de eventos para el botón Show Connection Statistics (Mostrar estadísticas de conexión).

2.2.2 Obtener las estadísticas de conexión del servidor SQL

El nuevo método SqlConnection.RetrieveStatistics averigua la instancia del servidor SQL con los datos de la conexión actual y devuelve un objeto IDictionary que contiene los 18 pares nombre/valor que muestra la siguiente figura.

los 18 pares nombre/valor que muestra la siguiente figura. Esta propiedad se ha de permitir explícitamente

Esta propiedad se ha de permitir explícitamente ejecutando una instrucción SqlConnec- tion.EnableStatistics=True antes de invocar el método RetrieveStatistic. El método más

sencillo para para tratar los valores nombre/valor es encrustar el objeto IDictionary en

un tipo HashTable y, después reiterar la tabla Hash en un bucle ForEach

go siguiente del proyecto BulkCopySameSchema.sln muestra en pantalla las estadísticas

en un cuadro de texto de un sencillo formulario frmConnStats:

Next . El códi-

Private Sub btnShowStats_Click(ByVal sender As System.Object, _ ByVal e As System.EventArgs) Handles btnShowStats.Click Try

htStats = CType(cnnNwind.RetrieveStatistics(), Hashtable) Dim txtStats As Control = frmConnStats.Controls.Item("txtStats") txtStats.Text = "" Dim oStat As Object Dim strStat As String For Each oStat In htStats.Keys strStat = oStat.ToString If InStr(strStat, "Time") > 0 Then txtStats.Text += strStat + " = " + _

50

Las novedades de ADO.NET 2.0

Microsoft.VisualBasic.Format(CLng(htStats(strStat)) /1000, _ "#,##0.000") + " secs" + vbCrLf

Else

txtStats.Text += strStat + " = " + htStats(strStat).ToString + vbCrLf

End If

Next frmConnStats.Show() frmConnStats.Controls.Item("btnClose").Focus() Catch excStats As Exception MsgBox(excStats.Message + excStats.StackTrace, , _ "Exception Displaying Connection Statistics") End Try End Sub

El código anterior y el formulario frmConnStats se pueden añadir a cualquier proyecto que utilice una SqlConnection. Invoque el método SqlConnection.ResetStatistics para ini- cializar los datos, excepto ConnectionTime.

Recuperar las estadísticas de conexión requiere establecer de nuevo una conexión con el ser- vidor, por lo tanto es mejor reservar el uso de esta función para diagnosticar problemas de conexión.

2.3 Ejecutar comandos SQL de forma asincrónica

ADO.NET 2.0 añade los métodos BeginExecuteReader, BeginExecuteXmlReader, y Begin- ExecuteNonQuery (junto con los correspondientes métodos End) para las clases Sql- Command. Estos métodos permiten ejecutar código mientras se espera a que un coman- do complete su ejecución. Para ejecutar un comando SqlCommand hay que añadir Async=True a la cadena de comando que se pasó al constructor de la SqlConnection. En los apartados siguientes se describe, con el correspondiente código de ejemplo, para los tres modelos de ejecución de comandos SqlCommand asíncronos que soporta la interfaz IasyncResul. La siguiente figura ilustra las bases de datos, conexiones y comandos que se utilizan con los tres modelos. Obtendrá resultados más interesantes del proyecto de ejemplo AsyncDataOperations.sln si dispone de dos o tres instancias del servidor SQL Server 2000 o 2005 con la base de datos de ejemplo de Northwind para cada instancia (figura en la página siguiente).

El proveedor de memoria compartida por defecto del SQLServer 2000 no soporta comando así- cronos, por lo que hay que utilizar localhost, y no (local), como valor para el servidor o la fuen- te de datos de la cadena de conexión en cualquier instancia local del SQLServer 2000.

Bases de datos con Visual Basic

Bases de datos con Visual Basic 2.3.1 El modelo Polling El modelo Polling es el más

2.3.1 El modelo Polling

El modelo Polling es el más sencillo de los tres. La siguiente figura ilustra el flujo del programa para tres conexiones asíncronas (figura en la página siguiente).

El código siguiente abre un comando asíncrono en la base de datos Northwind, en una instancia del servidor local SQL y utiliza un bucle While que consulta constantemente para que se complete el método BeginExecuteReader:

Private Sub PollingAsyncCommand() Try

Dim strConn As String = "Data Source=localhost;" + _ "Initial Catalog=Northwind;Integrated Security=SSPI;Async=True" Dim cnnCusts As SqlConnection = New SqlConnection(strConn) cnnCusts = New SqlConnection(strCusts) Dim cmdCusts As SqlCommand = cnnCusts.CreateCommand With cmdCusts .CommandType = CommandType.Text .CommandTimeout = 60 .CommandText = "SELECT * FROM Customers" End With

Dim asrCustsReader As IAsyncResult = _ cmdCusts.BeginExecuteReader(CommandBehavior.CloseConnection) While Not asrCustsReader.IsCompleted 'Do something while waiting

52

Las novedades de ADO.NET 2.0

Las novedades de ADO.NET 2.0 End While Dim sdrCusts As SqlDataReader = cmdCusts.EndExecuteReader(asrCustsReader) 'Do

End While Dim sdrCusts As SqlDataReader = cmdCusts.EndExecuteReader(asrCustsReader) 'Do something with the data sdrCusts.Close() sdrCusts.Dispose() Catch excAsync As Exception MsgBox(excAsync.Message + excAsync.StackTrace, , "Async Operation

Exception")

End Try

End Sub

La ejecución asíncrona con polling es muy práctica para las operaciones sencillas den- tro del bucle While, como mostrar una barra de progresión cuyo valor vienen definido por las pulsaciones e un contador. También se puede incluir código que permita al usuario cancelar un comando antes del tiempo indicado por su propiedad Com- mandTimeout. Al salir del bucle, la ejecución del mismo queda bloqueada hasta que se hayan completado todos los comandos o haya expirado su tiempo de ejecuión. El códi- go se va ejecutando en el hilo del formulario, por lo que los comandos múltiples se eje-

53

Bases de datos con Visual Basic

cutan secuencialmente en conexiones separadas. Si las operaciones múltiples Data- Reader.Read son complejas, se pueden ejecutar en un hilo dedicado al nuevo objeto BackgroundWorker. Esto permite invocar el siguiente método BeginExecuteReader inme- diatamente después de que la propiead IAsyncResult.IsComplete cambie a True.

2.3.2 El módelo Callback

El módelo asíncrono callback es más flexible que el polling porque utiliza un manejador de callback que ejecuta su propio hilo, extraído de la consulta. El modelo callback permi- te entrelazar comandos con bases de datos múltiples que se ejecutan en lo mismos ser- vidores o en servidores distintos. En ese caso, hay que especificar el tratador callback y pasarle el comando como objeto al segundo parámetro del método sobrecargado Be- ginExecuteReader. Al pasar el comando se tiene acceso al método EndExecuteReader con la propiedad IAsyncResult.AsyncState en el tratador callback. La siguiente figura mues- tra el flujo del programa en el modo callback. Las líneas punteadas indican la ejecución directa de los métodos Read, sin tener que esperar a que estén disponibles dotos los jue- gos de filas.

ejecución directa de los métodos Read , sin tener que esperar a que estén disponibles dotos

54

Las novedades de ADO.NET 2.0

A continuación mostramos un ejemplo de código para un comando asíncrono sencillo SqlCommand que usa el método callback:

Private Sub CallbackAsyncCommand() Try

Dim strConn As String = "Data Source=localhost;" + _ "Initial Catalog=Northwind;Integrated Security=SSPI;Async=True" Dim cnnCusts As SqlConnection = New SqlConnection(strConn) cnnCusts = New SqlConnection(strCusts) Dim cmdCusts As SqlCommand = cnnCusts.CreateCommand With cmdCusts .CommandType = CommandType.Text .CommandTimeout = 60 .CommandText = "SELECT * FROM Customers" End With cnnCusts.Open() Dim objCmdCusts As Object = CType(cmdCusts, Object) Dim asrCustsReader As IAsyncResult = _ cmdCusts.BeginExecuteReader(New AsyncCallback(AddressOf CustomersHandler), _ objCmdCusts, CommandBehavior.CloseConnection) Catch excAsync As Exception MsgBox(excAsync.Message + excAsync.StackTrace, , "Async Operation Exception")

End Try

End Sub

Y aquí está el código del tratador callback para el procedimiento anterior:

Private Sub CustomersHandler(ByVal iarResult As IAsyncResult) Try

Dim sdrData As SqlDataReader = CType(iarResult.AsyncState, SqlCommand).EndExecuteReader(iarResult)

With sdrData Dim intCtr As Integer While .Read For intCtr = 0 To .FieldCount - 1 objData = .GetValue(intCtr) Next intCtr End While .Close() .Dispose() End With Dim blnIsPool As Boolean = Thread.CurrentThread.IsThreadPoolThread CustomersDone(Thread.CurrentThread.ManagedThreadId, blnIsPool) Catch excHandler As Exception MsgBox(excHandler.Message + excHandler.StackTrace, , "Customers Handler Exception")

End Try

End Sub

55

Bases de datos con Visual Basic

La mayor parte de los ejemplos de cliente de este libro conectan con finales back en la misma máquina que el cliente; eso significa que la ejecución sincrónica de los DataReaders se comple- ta rápidamente o bien arroja inmediatamente una excepción. La ejecución asincrónica resulta especialmente eficaz en los proyectos con DataReaders múltiples que conectan individualemen- te a bases de datos remotas, especialmente si una o más conexiónes se ejecutan en un WAN.

El proyecto de ejemplo AsyncDataOperations.sln simula una aplicación de producción que conecta a bases de datos trabajadas en red múltiple estableciendo conexiones indi- viduales SqlConnections con tablas Northwind de clientes, pedidos y detalles de pedidos (Customers, Orders, y Order Details). Si tiene acceso a tres instancias de servidor SQL puede modificar las cadenas de conexión cambiando los nombres del segundo y el ter- cer servidor (OAKLEAF-W2K3 y OAKLEAF-MS2K3) por RemoteServerName, y seleccio- nar el cuadro de texto Use Multiple Instances para mostrar la secuencia de invocaciones de los métodos Connection.Open, BeginExecuteReader, y EndExecuteReader. La siguiente figura muestra dos instancias del formulario AsyncDataOperations.

muestra dos instancias del formulario AsyncDataOperations . Una clase timer VB.NET, escrita por Alastair Dallas,
muestra dos instancias del formulario AsyncDataOperations . Una clase timer VB.NET, escrita por Alastair Dallas,

Una clase timer VB.NET, escrita por Alastair Dallas, proporciona la resolución requerida para obtener datos de sincronización con sentido. Los números entre paréntesis de las entradas del cuadro de lista son los valores System.Threading.Thread .CurrentThread.ManagedThreadId de las instancias del formulario y los tres manejadores de callback El sufijo P indica que los hilos del manejador son del pool de hilos. La sincronización de datos es para una segunda ejecución (cacheada).

El código de ejemplo ejecuta objetos Customer desde el host local y objeto Orders y Order Details desde los servidores de red. La tabla Orders Details tiene unas 500.000 filas, por lo que leer toda la tabla lleva unos 2 segundos. La velocidad de ejecución en una LAN de bajo tráfico es normalmente suficiente para devolver los datos en la secuencia BeginExecuteReadercalling, como muestra la figura anterior (izquierda). Todas las opera- ciones de restablecimiento de datos se ejecutan en un solo hilo (13). Para simular una conexión WAN con la tabla Orders, el código en OrdersHandler provoca un retraso de unos segundos mediante múltiples operaciones en cada fila DataReader en un bucle ani-

56

Las novedades de ADO.NET 2.0

dado. En este caso, DataReader de Orders completa la ejecución antes que el DataReader de Customers, el cual termina la ejecución antes que el DataReader de Order Details, tal como muestra la figura anterior (derecha). En este caso, la restauración de Order Details se ejecuta en un hilo (14P), y Customers y Orders en otro distinto (13P).

El uso del modelo callback en las aplicaciones de formulario Windows es un tema controverti- do. Miembros del equipo de datos de VS 2005 de Microsoft recomiendan no utilizar este mode- lo con los proyectos de formulario de Windows. Los objetos de ADO.NET no son seguros en los hilos, y los problemas con hilos son difíciles de depurar.

2.3.3 El modelo WaitAll

Una alternativa al método callback es utilizar un array WaitHandle y asignarlo a un ele- mento en cada llamada de método BeginExecuteReader. Un WaitHandle.WaitAll(wh- Array) detiene la ejecución del código hasta que todos los DataReaders están listos para sus llamadas EndExecuteReader. Este comportamiento hace al modelo WaitAll especial- mente adecuado para clientes que procesan juegos de filas relacionados, ya que no se necesita el bucle de sincronización que se mostró anteriormente. La siguiente figura muestra el diagrama de flujo en el modelo WaitAll.

sincronización que se mostró anteriormente. La siguiente figura muestra el diagrama de flujo en el modelo

57

Bases de datos con Visual Basic

La manera más sencilla de ver los resultados del método WaitAll en un entorno de for- mulario Windows es crear una versión multi-hilo o multi-threaded apartment (MTA) de un procedimiento habitual Sub Main. Por defecto, los procedimientos de VB.NET utilizan el modelo de hilo único (single-threaded apartment, STA) requerido para los formularios basa- dos en Win32. Llamando WaitAll con múltiples WaitHandles, arroja una excepción dentro de los procedimiento STA, por lo que hay que añadir el prefijo <MTAThreadAttribute()> a la sentencia SharedSubMain. El siguiente listado es una adaptación del código del mode- lo callback para implementar el array multi-elemento WaitHandle:

<MTAThreadAttribute()> _ Shared Sub Main() Dim blnIsMultiServer As Boolean Try

cnnCusts = New SqlConnection(strCusts) Dim cmdCusts As SqlCommand = cnnCusts.CreateCommand With cmdCusts .CommandType = CommandType.Text .CommandTimeout = 10 .CommandText = "SELECT * FROM Customers" End With

If blnIsMultiServer Then cnnOrders = New SqlConnection(strOrders)

Else

cnnOrders = New SqlConnection(strCusts) End If Dim cmdOrders As SqlCommand = cnnOrders.CreateCommand With cmdOrders .CommandType = CommandType.Text .CommandTimeout = 10 .CommandText = "SELECT * FROM Orders" End With

If blnIsMultiServer Then cnnDetails = New SqlConnection(strDetails)

Else

cnnDetails = New SqlConnection(strCusts) End If Dim cmdDetails As SqlCommand = cnnDetails.CreateCommand With cmdDetails .CommandType = CommandType.Text .CommandTimeout = 10 .CommandText = "SELECT * FROM [Order Details]" End With

Dim timHiRes As New clsTimer timHiRes.Start()

58

Las novedades de ADO.NET 2.0

Dim awhHandle(2) As WaitHandle

cnnCusts.Open() astrListItems(0) = Format(timHiRes.ElapsedTime, "0.000") + " - 1 Opened Customers connection"

Dim asrCustomersReader As IAsyncResult asrCustomersReader = cmdCusts.BeginExecuteReader(CommandBehavior.CloseConnection) awhHandle(0) = asrCustomersReader.AsyncWaitHandle astrListItems(1) = Format(timHiRes.ElapsedTime, "0.000") + " - 2 BeginExecuteReader: Customers"

cnnOrders.Open() astrListItems(2) = Format(timHiRes.ElapsedTime, "0.000") + " - 3 Opened Orders connection"

Dim asrOrdersReader As IAsyncResult asrOrdersReader = cmdOrders.BeginExecuteReader(CommandBehavior.CloseConnection) awhHandle(1) = asrOrdersReader.AsyncWaitHandle

astrListItems(3) = Format(timHiRes.ElapsedTime, "0.000") + " - 4 BeginExecuteReader: Orders"

cnnDetails.Open() astrListItems(4) = Format(timHiRes.ElapsedTime, "0.000") + " - 5 Opened Details connection"

Dim asrDetailsReader As IAsyncResult asrDetailsReader = cmdDetails.BeginExecuteReader(CommandBehavior.CloseConnection) awhHandle(2) = asrDetailsReader.AsyncWaitHandle astrListItems(5) = Format(timHiRes.ElapsedTime, "0.000") + " - 6 BeginExecuteReader: Order Details"

WaitHandle.WaitAll(awhHandle) Dim sdrCustomers As SqlDataReader = cmdCusts.EndExecuteReader(asrCustomersReader) sdrCustomers.Close() sdrCustomers.Dispose() Dim sdrOrders As SqlDataReader = cmdOrders.EndExecuteReader(asrOrdersReader)

sdrOrders.Close() sdrOrders.Dispose() Dim sdrDetails As SqlDataReader = cmdDetails.EndExecuteReader(asrDetailsReader)

sdrDetails.Close()

59