¡Hola, encargados del scripting!¿Se puede salvar esta relación?

The Microsoft Scripting Guys

Descargar el código de este artículo: HeyScriptingGuy2007_09.exe (150KB)

Las relaciones importan. ¿Pero cómo hacen los encargados del scripting, entre todas las personas, para saber que las relaciones importan? Bueno, por una parte: miramos mucha televisión, y en la TV siempre nos dicen que las relaciones importan.

Y eso no sólo en telenovelas ni en telefilmes. Por ejemplo, hay un anuncio publicitario de una empresa de préstamos del día de paga que anima a las personas a que se conviertan clientes de su empresa. ¿Se deberá a que están dispuestos a prestar dinero a personas que antes que nada probablemente no deberían obtener un préstamo? No. ¿Se debe a que le cobrarán hasta un 900 por ciento de interés anual en ese préstamo? Por supuesto que no. Se debe a que esta empresa de préstamos del día de paga forma una relación personal con usted. Como su anuncio lo indica, "las relaciones importan".

Nota: Seguro, estamos llorando, no nos avergonzamos de eso. A fin de cuentas, tendría que ser un poco frío e insensible para no llorar un poco sólo con pensar en los lazos profundos y perdurables que vinculan a una persona con su empresa de préstamos del día de paga.

Sabemos lo que está pensando, por supuesto: "Cielos, si las relaciones le importan a las empresas de préstamos del día de paga, entonces las relaciones deben realmente importarle a las personas que escriben scripts que interactúan con bases de datos". Y el hecho de que importen se debe a que las relaciones deberían importarle a las personas que escriben scripts que interactúan con las bases de datos. Sin embargo, desafortunadamente, estos escritores de script no siempre se dan cuenta de que las relaciones importan. Pero no pasa nada, en el artículo de este mes lo dejaremos bien en claro para todos.

Sabemos que muchos administradores de sistema usan las bases de datos, a menudo como una manera de realizar un seguimiento de su inventario de hardware. El problema es que muchas de estas bases de datos se configuran de un modo algo menos que óptimo. Por ejemplo, supongamos que necesita una base de datos que realice un seguimiento de las unidades de disco conectadas a todos los equipos. Si no cuenta con información previa en diseño de base de datos, es bastante probable que cree una base de datos de una tabla como la mostrada en la figura 1.

Figura 1 Base de datos de una tabla

Figura 1** Base de datos de una tabla **

Como puede ver, es un diseño muy sencillo: Hay un campo para el nombre de equipo, y un par de campos Sí/No que se usan para indicar si un equipo posee una unidad C: y una unidad D:. Y eso es todo. Preciso, claro y al punto.

Entonces, ¿cuál es el problema con esta base de datos? Bueno, a decir verdad, casi todo. Si puede estar seguro que ninguno de sus equipos posee más de dos unidades de disco, este diseño funcionará (casi). Sin embargo, ¿qué sucede si tiene un equipo que posee tres unidades de disco? En ese caso, tendrá que agregar otro campo, que realice un seguimiento de la unidad de disco E. De acuerdo, pero qué sucede si tiene un equipo que posee once unidades de disco? ¿Y qué si desea realizar un seguimiento de las propiedades de cada una de esas unidades de disco? Por ejemplo, supongamos que quiere realizar un seguimiento del tamaño total de cada unidad de disco. En ese caso, necesitará un campo Drive_C_Size. Y un campo Drive_D_Size. Y un campo Drive_E_Size, y... bueno, ya se hace una idea. (E imagínese que desea realizar un seguimiento del espacio en disco disponible, tipo de conector del disco, si se han habilitado cuotas de disco en la unidad de disco, etc.).

Aquí una regla empírica a tener presente: cada vez que una entidad posea más que una unidad de algo (por ejemplo, un equipo puede tener más de una unidad de disco), este diseño de "archivo plano" (una tabla) no está muy bien. Es cierto, en el caso de las unidades de disco quizás piense que con esto es suficiente; a fin de cuentas, sólo tendrá un número finito de unidades de disco conectadas a un equipo. Pero supongamos que quiere consultar su base de datos y obtener una lista de todas las unidades de disco iguales o mayores que 100 gigabytes. ¿Cómo tendría que hacer para realizar algo así? Bueno, primero tendría que buscar unidades de 100 gigabytes en el campo Drive_C_Size, luego buscar lo mismo en el campo Drive_D_Size, en el campo Drive_E_Size, en... En general, no es un método muy eficiente o eficaz. ¿Pero cuál es la alternativa? Si no puede usar una base de datos de archivo plano, ¿qué puede usar?

Es fácil: una base de datos relacional. Las bases de datos relacionales se diseñan, entre otros aspectos, para atender relaciones uno a varios (por ejemplo, un equipo que posea muchas unidades de disco). Y antes de que pregunte: no, no tiene que salir y comprar nada nuevo para poder usar una base de datos relacional; si dispone de una base de datos de algún tipo (Microsoft® Access™, SQL Server™, Oracle o lo que sea), es probable que ya tenga una base de datos relacional. No necesita comprar nada; sólo necesita conocer dos cosas: 1) cómo establecer una relación entre dos tablas en esa base de datos; y 2) cómo escribir un script que pueda aprovechar esa relación.

Nota: Según la televisión nunca debe intentar aprovechar una relación. Pero esta tarea es diferente.

Debido a que este artículo no se llama ¡Hola, encargados del diseño de base de datos!, no dedicaremos mucho tiempo para tratar el diseño de base de datos. En cambio, simplemente le mostraremos un diseño muy sencillo de base de datos relacional que nos permitirá hablar sobre escritura de consultas relacionales. En esta base de datos tenemos dos tablas. La primera se denomina Computers y contiene un par de campos: ComputerName y SerialNumber. El campo ComputerName es donde almacenaremos el nombre de equipo y el campo SerialNumber es donde almacenaremos... bueno seguro puede adivinarlo. Exacto, el número de serie.

Entonces, ¿dónde podemos poner la información de nuestra unidad de disco? Va en nuestra segunda tabla, que se denomina DiskDrives. Esta tabla contiene tres campos: SerialNumber que contiene el número de serie, DriveLetter que contiene la letra de la unidad y DriveSize que contiene el tamaño de la unidad.

Aquí el campo clave es SerialNumber. Si ese nombre de campo suena familiar, bueno, debe ser así: tenemos exactamente el mismo campo en la tabla Computers. Y eso no es un accidente ni una coincidencia; lo hicimos a propósito. Para establecer una relación entre estas dos tablas necesitamos tener un campo que aparezca en cada tabla; ese es el modo en que podemos decir qué unidades de disco pertenecen a qué equipos.

¿Por qué vinculamos las dos tablas mediante el número de serie en lugar de, por ejemplo, el nombre de equipo? En realidad hay una buena razón para ello: los nombres de equipo pueden cambiar (y a menudo lo hacen). Los números de serie típicamente no.

Pero ya es suficiente discurso. Echemos una mirada a la figura 2, un script que puede buscar estas dos tablas y recuperar una lista de las unidades de disco que pertenecen a cada equipo. La figura 3 muestra la clase de datos que este script devuelve.

Figure 3 Resultados de búsqueda

atl-ws-01
C:

atl-ws-01
D:

atl-ws-01
E:

atl-ws-02
C:

atl-ws-02
D:

Figure 2 Búsqueda en las tablas

Const adOpenStatic = 3
Const adLockOptimistic = 3

Set objConnection = CreateObject("ADODB.Connection")
Set objRecordSet = CreateObject("ADODB.Recordset")

objConnection.Open _
 "Provider = Microsoft.Jet.OLEDB.4.0; " & _
 "Data Source = c:\scripts\test.mdb" 

objRecordSet.Open "SELECT Computers.*, DiskDrives.* FROM Computers " & _
 "INNER JOIN DiskDrives ON Computers.SerialNumber = DiskDrives.SerialNumber " & _
 "ORDER BY Computers.ComputerName, DiskDrives.Drive", objConnection, adOpenStatic, _
  adLockOptimistic

objRecordSet.MoveFirst

Do Until objRecordset.EOF
 Wscript.Echo objRecordset.Fields.Item("ComputerName")
 Wscript.Echo objRecordset.Fields.Item("Drive")
 Wscript.Echo
 objRecordset.MoveNext
Loop

objRecordSet.Close
objConnection.Close

No analizaremos los fundamentos para conectarse y trabajar con bases de datos en el artículo de este mes; si es principiante en el scripting de base de datos quizás puede echar una mirada a la webcast de los encargados del scripting "Scripting de base de datos para administradores del sistema" (go.microsoft.com/fwlink/?LinkId=22089). Todo lo que diremos es que nos conectamos a una base de datos Access (C:\Scripts\Test.mdb) y trabajamos con tablas relacionales denominadas Computers y DiskDrives. Esto se aclarará lo suficiente a partir del script.

De acuerdo, algo más que diremos es que necesita realizar un pequeño cambio para que funcione con Access 2007: Es importante cambiar el proveedor de Microsoft.Jet.OLEDB.4.0 a Microsoft.ACE.OLEDB.12.0, del siguiente modo:

objConnection.Open _
 "Provider = Microsoft.ACE.OLEDB.12.0; " & _
 "Data Source = c:\scripts\test.accdb" 

Eso es todo. Lo que haremos, en cambio, es centrarnos en la consulta SQL que recupera los datos de nuestras dos tablas:

objRecordSet.Open "SELECT Computers.*, " & _
 "DiskDrives.* FROM Computers " & _
 "INNER JOIN DiskDrives ON " & _
 "Computers.SerialNumber = " & _
 "DiskDrives.SerialNumber " & _
 "ORDER BY ComputerName, Drive", _
 objConnection, adOpenStatic, adLockOptimistic 

¿Complicado? Quizás un poco. Así que veremos si podemos desglosarlo un poco y hacerlo más fácil de digerir.

La primera parte de nuestra consulta es bastante fácil en realidad. Deseamos seleccionar todos los campos en ambas tablas: Computers y DiskDrives. De ahí que se necesita este fragmento de SQL:

SELECT Computers.*, DiskDrives.* 

No está nada mal; el asterisco, aunque no es necesario decirlo, simplemente es un carácter comodín que significa "todo".

Si ha escrito antes consultas SQL (o si ha escrito consultas WMI, Instrumental de administración de Windows®, que usan un subconjunto del lenguaje de consulta SQL), conoce la rutina: después de especificar los elementos que desea seleccionar, especifica a continuación desde dónde desea seleccionar estos elementos. Entonces, la pieza siguiente de nuestro rompecabezas de la consulta es:

FROM Computers INNER JOIN DiskDrives ON Computers.SerialNumber = DiskDrives.SerialNumber

No permita que le intimide. Es cierto, es más complicado que una típica instrucción FROM, pero por una buena razón. Después de todo, en una consulta SQL típica sólo seleccionamos los datos de una tabla; en esta oportunidad seleccionamos los datos de dos tablas a la vez.

Vamos a examinar más atentamente la sintaxis y ver cómo funciona. Como puede ver, pedimos que el script seleccione los datos desde la tabla Computers y desde la tabla DiskDrives. Sin embargo, observe que no usamos la palabra "and"; en cambio, usamos el término INNER JOIN. Este término define el tipo de relación y, por su parte, describe cómo combinaremos información de dos tablas separadas en un solo conjunto de registros. (Y sí, hay otros tipos de combinaciones; hablaremos de ellas en unos pocos minutos).

Por lo que hemos dicho, depende de nosotros especificar el tipo de relación que existe entre nuestras tablas; también depende de nosotros especificar exactamente cómo se relacionan esas tablas. De eso se encarga este fragmento de código:

ON Computers.SerialNumber = DiskDrives.SerialNumber

Lo que hacemos aquí es definir cómo combinaremos nuestras tablas. Los registros se agruparán en cualquier momento que el campo SerialNumber en la tabla Computers coincida con el campo SerialNumber en la tabla DiskDrives. ¿Qué sucede si hubiéramos usado un campo diferente (digamos, ComputerName) como nuestro campo de combinación? Bueno, entonces nuestro código tendría el aspecto siguiente:

ON Computers.ComputerName = DiskDrives.ComputerName

En caso de que lo pregunte: no, los dos campos no tienen que tener el mismo nombre; sólo deben contener los mismos datos. Usar el mismo nombre facilita la identificación del campo relacional. Hay sólo un pequeño truco a tener en cuenta: debido a que tenemos dos campos con el mismo nombre, necesitamos usar siempre la sintaxis TableName.Field.Name cuando hagamos referencia a uno de estos campos. Eso significa usar un código similar al siguiente: Wscript.Echo obj- Recordset.Fields.Item("Computers.SerialNumber").

En realidad eso es todo lo que necesitamos; el resto de la consulta solamente clasifica los datos, primero por nombre de equipo y, a continuación, por unidad de disco:

ORDER BY ComputerName, Drive

No fue tan terrible, ¿o sí? Ahora debemos hacer una pausa por algunos segundos y hablar sobre combinaciones. ¿Por qué usamos una combinación interna en nuestra consulta? Muy fácil. Una combinación interna sólo devuelve los registros que tienen valores que coinciden en cada tabla. Es decir, supongamos que tenemos el número de serie 121989. Si ese número de serie aparece en ambas tablas, Computers y DiskDrives, se devolverán los registros correspondientes. (Por supuesto, el número de serie no puede aparecer en cualquier parte; debe estar en el campo SerialNumber). ¿Tiene sentido? Bien.

Ahora, supongamos que tenemos un equipo que tiene el número de serie 121989; sin embargo, no hay unidades de disco con ese número de serie. En ese caso, para el equipo con el número de serie 121989 no se devolverá ningún dato. Esto se debe a que una combinación interna sólo devuelve información si un registro tiene valores que coinciden en cada una de las tablas combinadas.

Es decir, una consulta con combinación interna devuelve todos los equipos que tienen discos duros; no devolverá ningún equipo que no tenga discos duros, ni ningún disco duro que no esté instalado en un equipo. Eso es bueno; a menudo es lo que deseamos. ¿Pero qué sucede si desea obtener una lista de equipos que no tienen discos duros o discos duros que no tienen equipos? ¿Entonces qué?

Para eso sirve la combinación externa. (Ah, ¡para eso sirve la combinación externa!) De momento, diremos que sólo existen dos tipos diferentes de combinaciones externas: Izquierdo y derecho. En nuestra base de datos tenemos dos tablas: Computers (que se considera la tabla "izquierda" porque es la tabla maestra) y DiskDrives (que se considera la tabla "derecha" porque es, bueno, porque no es la tabla maestra). Supongamos que deseamos que nuestro conjunto de registros devuelto incluya todos los equipos, incluso aquellos que no tienen instalada ninguna unidad de disco. En ese caso, usamos la sintaxis LEFT OUTER JOIN y una consulta similar a la siguiente:

objRecordSet.Open "SELECT Computers.*, " & _
 "DiskDrives.* FROM Computers " & _
 "LEFT OUTER JOIN DiskDrives ON " & _
 "Computers.SerialNumber = " & _
 "DiskDrives.SerialNumber " & _
 "ORDER BY ComputerName, Drive", _
 objConnection, adOpenStatic, adLockOptimistic

Como se habrá imaginado, una combinación externa izquierda devuelve todos los registros en la tabla izquierda, aún si no existen registros correspondientes en la tabla derecha. Así, obtenemos todos los equipos, pero sólo los registros de la tabla derecha (unidades de disco) que están asociados con un equipo.

Alternativamente, quizá deseamos una lista de todas las unidades de disco, incluidas aquellas que no están instaladas en un equipo. Debido a que la tabla DiskDrives es la tabla derecha en la relación, usamos la sintaxis RIGHT OUTER JOIN, tal como se indica a continuación:

objRecordSet.Open "SELECT Computers.*, " & _
 "DiskDrives.* FROM Computers " & _
 "RIGHT OUTER JOIN DiskDrives ON " & _
 "Computers.SerialNumber = " & _
 "DiskDrives.SerialNumber " & _
 "ORDER BY ComputerName, Drive", _
 objConnection, adOpenStatic, adLockOptimistic

Ahora que pensamos en esto, si DiskDrives es la tabla derecha en la relación, entonces con todo derecho Computers se debería conocer como la tabla marido en la relación en vez de la tabla izquierda. Como algunos encargados del scripting saben a través de la dolorosa experiencia, el marido nunca es la entidad correcta en una relación.

Con la combinación externa derecha, obtendremos todos los registros en la tabla derecha (todas las unidades de disco), pero sólo los equipos (registros en la tabla izquierda) que están asociados con una unidad de disco.

Esto quizás no tenga mucho sentido por el momento, pero si juega un poco verá cómo funciona. Si necesita una base de datos con que jugar, hemos publicado una de ejemplo en la dirección microsoft.com/technet/scriptcenter/resources/tnmag/archive.mspx.

Es importante mencionar que cada vez que use una combinación externa deberá también, al menos, hacer que On Error Resume Next sea la primera línea en su script. ¿Por qué? Bueno, supongamos que realizamos una combinación externa izquierda; en ese caso es posible que obtengamos algunos equipos que no tienen ningún disco duro instalado. Está bien (después de todo, eso es lo que deseamos obtener), por lo menos hasta que encontremos esta línea de código, que devuelve la letra de unidad:

Wscript.Echo objRecordset.Fields.Item("Drive")

Debido a que no existe ningún campo Drive para este equipo en particular, el script finalizará con una detención por bloqueo.

C:\Scripts\database.vbs(22, 9) Microsoft VBScript runtime error: Type mismatch

Sin embargo, si implementamos On Error Resume Next, el script puede omitir el hecho que un equipo no tenga unidades de disco y puede continuar alegremente su camino. Como alternativa, podría usar código similar a éste para comprobar el valor del campo Drive y posteriormente tomar la acción apropiada:

If IsNull(objRecordset.Fields.Item("Drive")) _Then
    Wscript.Echo "No disk drives installed."
Else
    Wscript.Echo _
      objRecordset.Fields.Item("Drive")
End If

Con este código podemos comprobar si devolvemos un valor Null en contraposición con una letra de unidad real. Si ese es el caso, entonces enviamos un mensaje tipo "no hay unidades de disco instaladas". Si no obtenemos un valor Null, entonces simplemente enviamos el valor del campo Drive. El resultado neto es la salida, que se muestra en la figura 4.

Figure 4 Mostrar los resultados apropiados

atl-ws-01
C:

atl-ws-01
D:

atl-ws-02
C:

atl-ws-02
D:

alt-ws-03
No disk drives installed.

Sin entrar en demasiadas explicaciones, veamos un par de consultas más. Por ejemplo, esta es una consulta con combinación interna que devuelve una lista de equipos y sus unidades de disco instaladas, siempre que esas unidades de disco sean mayores que 50 gigabytes (suponemos que los tamaños de unidad de disco se almacenan en gigabytes):

objRecordSet.Open "SELECT Computers.*, " & _
 "DiskDrives.* FROM Computers " & _
 "INNER JOIN DiskDrives ON " & _
 "Computers.SerialNumber = " & _
 "DiskDrives.SerialNumber " & _
 "WHERE DriveSize > 50 " & _
 "ORDER BY Computers.ComputerName, " & _
 "DiskDrives.Drive", objConnection, _
 adOpenStatic, adLockOptimistic

Como puede observar, todo lo que hicimos fue agregar una cláusula WHERE a la consulta original:

WHERE DriveSize > 50

¿Qué sucede si sólo deseamos obtener información sobre la unidad de disco E en los equipos? Ningún problema; sólo agregue la cláusula Where apropiada:

WHERE Drive = 'E:'

Y esta es una consulta un poco más complicada, que devuelve una colección de equipos que no tienen unidades de disco instaladas:

objRecordSet.Open _
 "SELECT Computers.ComputerName, " & _
 "DiskDrives.Drive " & _
 "FROM Computers LEFT JOIN DiskDrives " & _
 "ON Computers.SerialNumber = " & _
 "DiskDrives.SerialNumber " & _
 "GROUP BY Computers.ComputerName, " & _
 "DiskDrives.Drive " & _
 "HAVING (DiskDrives.Drive) Is Null " & _
 "ORDER BY Computers.ComputerName", _
 objConnection, adOpenStatic, adLockOptimistic

Como dijimos, es un poco más complicada, y como nos quedamos sin espacio no explicaremos cómo funciona. Pero funciona, que es lo más importante.

Bien, quizás sea lo segundo más importante; como hemos indicado varias veces, lo que realmente importa son las relaciones. Lo cual no significa que las relaciones no puedan salir mal. Como Woody Allen dijo al final de "Annie Hall", "Una relación... es como un tiburón; tiene que moverse constantemente hacia adelante o se muere. Y pienso que lo que tuvimos en nuestras manos es un tiburón muerto". Ah, si Woody sólo hubiera sabido sobre combinaciones internas y combinaciones externas. ¡Con estas técnicas de consulta, las relaciones siempre serán correctas!

The Microsoft Scripting Guys trabajan para Microsoft, mejor dicho, están contratados por Microsoft. Cuando no juegan al béisbol, ni entrenan ni lo ven (u otras actividades varias), dirigen el TechNet Script Center. Visite la página web www.scriptingguys.com.

© 2008 Microsoft Corporation and CMP Media, LLC. Reservados todos los derechos; queda prohibida la reproducción parcial o total sin previa autorización.