Hey, Scripting Guy!Ist diese Beziehung noch zu retten?

The Microsoft Scripting Guys

Laden Sie den Code für diesen Artikel herunter: HeyScriptingGuy2007_09.exe (150KB)

Beziehungen sind wichtig. Aber wieso sollten sich ausgerechnet die Scripting Guys mit der Bedeutung von Beziehungen auskennen? Nun ja, zum einen sehen wir viel fern, und im Fernsehen erzählen sie immer, dass Beziehungen wichtig sind.

Und das nicht nur in Seifenopern oder den Fernsehfilmen während der Woche. So gibt es zum Beispiel eine Werbung eines Kreditbüros für Barkredite, die Menschen ermuntert, ihr Geschäft zu beehren. Machen die das etwa, weil sie bereit sind, an Menschen Geld zu verleihen, die sonst wahrscheinlich kein Darlehen bekämen? Nein. Oder weil sie Ihnen für dieses Darlehen so gut wie 900 % Zinsen jährlich berechnen? Selbstverständlich nicht. Sie machen es, weil dieses Kreditunternehmen eine persönliche Beziehung mit Ihnen aufbaut. Wie es in ihrer Werbung so schön heißt: „Beziehungen sind wichtig“.

Hinweis: Ja, uns kommen die Tränen, und wir schämen uns nicht dafür. Schließlich müsste man recht kalt und abgebrüht sein, um nicht schon beim Gedanken an die tiefen, dauerhaften Bande, die einen Menschen mit seinem Kreditunternehmen verbinden, ein wenig aufgewühlt zu werden.

Selbstverständlich wissen wir, was Sie jetzt denken: „Mensch, wenn schon Beziehungen zu Kreditunternehmen so wichtig sind, dann müssen sie ja Leuten, die Datenbankskripts schreiben, wirklich viel bedeuten.“ So ist es tatsächlich: Wenn Sie Datenbankskripts schreiben, sollten Ihnen Beziehungen wichtig sein. Aber leider ist das nicht jedem Skriptersteller bewusst. Kein Problem, denn im Artikel dieses Monats werden wir das jedem klarmachen.

Wir wissen, dass viele Systemadministratoren Datenbanken verwenden, oft als Möglichkeit, ihren Hardwarebestand zu überwachen. Das Problem ist, dass viele dieser Datenbanken nicht gerade ideal eingerichtet wurden. Nehmen Sie zum Beispiel einmal an, dass Sie eine Datenbank benötigen, die alle in Ihrem Netzwerk vorhandenen Laufwerke überwacht. Wenn Sie kein Hintergrundwissen im Entwerfen von Datenbanken haben, ist es sehr wahrscheinlich, dass Sie eine Eintabellendatenbank erstellen werden (siehe Abbildung 1).

Abbildung 1 Eintabellendatenbank

Abbildung 1** Eintabellendatenbank **

Wie Sie sehen, ist das ein sehr einfacher Entwurf: Es gibt ein Feld für den Computernamen und einige Ja/Nein-Felder, die verwendet werden, um anzuzeigen, ob ein Computer ein Laufwerk C: und ein Laufwerk D: hat. Das ist schon alles. Knapp, klar und auf den Punkt.

Was ist also falsch an dieser Datenbank? Um aufrichtig zu sein: so ziemlich alles. Wenn Sie sicher sein können, dass keiner Ihrer Computer mehr als zwei Laufwerke hat, wird dieser Entwurf einigermaßen funktionieren. Was geschieht jedoch, wenn Sie einen Computer haben, der drei Laufwerke hat? In diesem Fall müssen Sie ein weiteres Feld für Laufwerk E hinzufügen. Was machen Sie aber bei einem Computer mit elf Laufwerken? Was ist, wenn Sie die Eigenschaften jedes Laufwerks aufzeichnen wollen? Nehmen Sie zum Beispiel einmal an, dass Sie die Gesamtkapazität eines jeden Laufwerks aufzeichnen möchten. In diesem Fall benötigen Sie das Feld „Laufwerk_C_Größe“. Und das Feld „Laufwerk_D_Größe“. Und ein Feld „Laufwerk_E_Größe“ und so weiter und so fort. (Womöglich möchten Sie auch den verfügbaren Speicherplatz überwachen, die Art der Datenträgerverbindung, ob Datenträgerkontingente auf dem Laufwerk aktiviert worden sind und so weiter).

Hier ist eine Faustregel zu bedenken: Wenn eine Entität von einer Elementart mehr als eines besitzen kann (zum Beispiel kann ein Computer mehr als ein Laufwerk haben), sind Flatfiletabellen nicht sehr gut geeignet. Im Fall von Laufwerken könnten Sie denken, dass Sie damit durchkommen. Schließlich werden Sie nur eine begrenzte Anzahl von Laufwerken an einem Computer angeschlossen haben. Aber nehmen Sie einmal an, dass Sie Ihre Datenbank abfragen und eine Liste aller Laufwerke erhalten wollen, die 100 GB oder größer sind. Wie würden Sie vorgehen, um so etwas zu machen? Nun, Sie müssten zuerst nach 100-GB-Laufwerken im Feld „Laufwerk_C_Größe“ suchen, dann das gleiche im Feld „Laufwerk_D_Größe“, dann das Feld „Laufwerk_E_Größe“... Alles in allem kein sehr effizienter oder wirksamer Ansatz. Aber was ist die Alternative? Wenn Sie keine Flatfiledatenbank verwenden können, was dann?

Die Antwort ist ganz einfach: eine relationale Datenbank. Relationale Datenbanken sind unter anderem auf die Verarbeitung von 1:n-Beziehungen ausgelegt (z. B. Computer mit vielen Laufwerken). Zur Verwendung einer relationalen Datenbank müssen Sie keine neuen Technologien kaufen. Wenn Sie eine beliebige Datenbank haben (Microsoft® Access™, SQL-Server™, Oracle usw.), stehen die Chancen gut, dass Sie bereits eine relationale Datenbank haben. Sie müssen nur zwei Dinge wissen: 1) Wie in dieser Datenbank eine Beziehung zwischen zwei Tabellen aufgestellt wird. 2) Wie ein Skript geschrieben wird, das diese Beziehung nutzen kann.

Hinweis: Den Fernsehsendungen nach sollte man nie versuchen, eine Beziehung auszunutzen. Aber hier ist es anders.

Diese Rubrik heißt nicht „Hey, Datenbankdesign-Guy!“. Deshalb werden wir nicht viel Zeit mit Diskussionen über den Datenbankentwurf verbringen. Stattdessen werden wir Ihnen nur einen sehr einfachen relationalen Datenbankentwurf zeigen, der uns ermöglicht, das Schreiben relationaler Abfragen zu besprechen. In dieser Datenbank haben wir zwei Tabellen. Die erste heißt „Computer“, und besteht aus zwei Feldern: ComputerName und SerialNumber. Im Feld „ComputerName“ werden wir den Computernamen speichern, und im Feld „SerialNumber“ speichern wir die Seriennummer.

Wo geben wir dann unsere Laufwerkinformationen ein? Diese kommen in unsere zweite Tabelle, die „DiskDrives“ heißt. Sie enthält drei einfache Felder: SerialNumber, DriveLetter und DriveSize.

Das Hauptfeld hier ist „SerialNumber“. Wenn dieser Feldname vertraut klingt, dann ist das auch so gewollt: Wir haben genau das gleiche Feld in der Tabelle „Computer“. Das ist kein Zufall. Um eine Beziehung zwischen diesen zwei Tabellen einzurichten, müssen wir ein Feld haben, das in beiden erscheint. So können wir feststellen, welche Laufwerke zu welchen Computern gehören.

Weshalb haben wir die zwei Tabellen über die Seriennummer und nicht den Computernamen verknüpft? Dafür gibt es einen guten Grund: Computernamen können sich ändern (und das geschieht oft). Seriennummern normalerweise nicht.

Aber genug geredet. Sehen wir uns Abbildung 2 an, ein Skript, das diese zwei Tabellen durchsuchen, und eine Liste der Laufwerke abrufen kann, die zu jedem Computer gehören. Abbildung 3 zeigt die Arten von Daten, welche dieses Skript ausgibt.

Figure 3 Suchergebnisse

atl-ws-01
C:

atl-ws-01
D:

atl-ws-01
E:

atl-ws-02
C:

atl-ws-02
D:

Figure 2 Durchsuchen der Tabellen

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

Grundlegendes zur Herstellung einer Verbindung und Arbeit mit Datenbanken werden in dieser Ausgabe nicht behandelt. Wenn Sie beim Datenbankskripting neu sind, sehen Sie sich am besten den Scripting Guys-Webcast „Datenbankskripting für Systemadministratoren“ an (go.microsoft.com/fwlink/?LinkId=22089). In diesem Beispiel wird lediglich eine Verbindung zu einer Access-Datenbank (C:\Scripts\Test.mdb) hergestellt und mit relationalen Tabellen namens „Computer“ und „DiskDrives“ gearbeitet. So viel sollte eigentlich aus dem Skript klar sein.

Es ist noch zu erwähnen, dass Sie eine kleine Änderung vornehmen müssen, damit es mit Access 2007 funktioniert: Es ist wichtig, den Anbieter von Microsoft.Jet.OLEDB.4.0 in Microsoft.ACE.OLEDB.12.0 zu ändern, und zwar folgendermaßen:

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

Das ist alles. Stattdessen werden wir uns auf die SQL-Abfrage konzentrieren, die Daten von unseren zwei Tabellen abruft:

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

Kompliziert? Vielleicht ein wenig. Sehen wir also zu, ob wir dies nicht ein wenig unterteilen können, um es leichter verdaulich zu machen.

Der erste Teil unserer Abfrage ist tatsächlich ziemlich leicht. Wir wollen alle Felder in beiden Tabellen auswählen: Computer und DiskDrives. Deshalb nur so viel von SQL:

SELECT Computers.*, DiskDrives.* 

Das Sternchen, versteht sich, ist einfach ein Platzhalterzeichen, das „alles“ bedeutet.

Wenn Sie schon einmal SQL-Abfragen geschrieben haben (oder wenn Sie Windows® Management Instrumentation oder WMI-Abfragen geschrieben haben, die eine Teilmenge der SQL-Abfragesprache verwenden), kennen Sie die Routine: Nach dem Angeben der Elemente, die Sie auswählen wollen, geben Sie an, von wo aus Sie diese Elemente auswählen wollen. Daher der nächste Teil der Abfrage:

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

Lassen Sie sich dadurch nicht aus dem Konzept bringen. Ja, es ist komplizierter als eine typische FROM-Anweisung, aber aus gutem Grund. In einer typischen SQL-Abfrage werden nur Daten aus einer Tabelle ausgewählt; diesmal wählen wir Daten aus zwei Tabellen gleichzeitig aus.

Sehen wir uns die Syntax näher an und schauen, wie es funktioniert. Wie Sie sehen, soll das Skript Daten aus den Tabellen „Computer“ und „DiskDrives“ auswählen. Beachten Sie aber, dass nicht das Wort „and“ sondern „INNER JOIN“ verwendet wird. Dieser Begriff definiert die Art der Beziehung und beschreibt seinerseits, wie Informationen von zwei separaten Tabellen in einem einzigen Datensatz verknüpft werden. (Es gibt weitere Verknüpfungstypen; diese werden gleich erörtert.)

Wie bereits angedeutet, liegt es an uns, die Art der Beziehung anzugeben, die zwischen den Tabellen existiert. Das wird durch folgendes Codeelement erreicht:

ON Computers.SerialNumber = DiskDrives.SerialNumber

Hier wird definiert, wie die Tabellen verknüpft sind. Jedes Mal, wenn das Feld „SerialNumber“ in der Tabelle „Computer“ dem Feld „SerialNumber“ in der Tabelle „DiskDrives“ entspricht, werden Datensätze zusammen gruppiert. Was wäre, wenn wir ein anderes Feld (etwa „ComputerName“) als Verknüpfungsfeld verwendet hätten? Der Code würde dann so aussehen:

ON Computers.ComputerName = DiskDrives.ComputerName

Die beiden Felder müssen nicht den gleichen Namen haben, sie müssen nur die gleichen Daten enthalten. Die Verwendung desselben Namens erleichtert die Identifizierung des relationalen Felds. Dabei gibt es einen kleinen Haken: Weil wir zwei Felder mit dem gleichen Namen haben, muss immer die Syntax „TabellenName.FeldName“ verwendet werden, wenn wir uns auf eines dieser Felder beziehen. Der Code muss daher folgendermaßen aussehen: Wscript.Echo obj- Recordset.Fields.Item("Computers.SerialNumber").

Das ist wirklich alles, was wir benötigen. Die restliche Abfrage sortiert lediglich die Daten, zuerst nach Computernamen und dann nach Laufwerk:

ORDER BY ComputerName, Drive

Das ist doch ganz einfach oder nicht? Hier müssen wir kurz innehalten und über Verknüpfungen sprechen. Warum haben wir in unserer Abfrage eine innere Verknüpfung verwendet? Die Antwort ist ganz einfach: Eine innere Verknüpfung gibt nur Datensätze an, die passende Werte in jeder Tabelle haben. Anders gesagt: Wenn beispielsweise die Seriennummer 121989 sowohl in der Tabelle „Computer“ als auch in der Tabelle „DiskDrives“ erscheint, werden die entsprechenden Datensätze angegeben. (Selbstverständlich kann die Seriennummer nicht irgendwo erscheinen. Sie muss im Feld „SerialNumber“ enthalten sein). Ist dies verständlich? Gut.

Nehmen Sie jetzt an, dass wir einen Computer haben, der die Seriennummer 121989 hat, aber es gibt keine Laufwerke mit dieser Seriennummer. In diesem Fall werden keine Daten für den Computer mit Seriennummer 121989 angegeben. Das kommt daher, dass eine innere Verknüpfung nur dann Informationen ausgibt, wenn ein Datensatz passende Werte in jeder der verknüpften Tabellen hat.

Anders gesagt gibt eine Abfrage mit innerer Verknüpfung alle Computer aus, die Festplatten haben, jedoch keine Computer ohne Festplatte und keine Festplatten, die nicht in einem Computer installiert sind. In den meisten Fällen ist diese Abfrage geeignet. Aber was ist, wenn Sie eine Liste der Computer wollen, die keine Festplatten haben, oder der Festplatten, die nicht in Computern installiert sind? Was dann?

Dafür ist die äußere Verknüpfung da. (Ach so, dafür ist also die äußere Verknüpfung da!) Vorerst ist dazu nur zu sagen, dass es nur zwei verschiedene Typen äußerer Verknüpfung gibt: linke und rechte. In dieser Datenbank gibt es zwei Tabellen: Computer (als linke Tabelle angesehen, weil sie die Haupttabelle ist) und DiskDrives (als rechte Tabelle angesehen, weil sie nicht die Haupttabelle ist). Angenommen, die Abfrage soll alle Computer umfassen, auch die, in denen keine Laufwerke installiert sind. In diesem Fall wird die Syntax „LEFT OUTER JOIN“ verwendet und eine Abfrage, die folgendermaßen aussieht:

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

Wie Sie wahrscheinlich schon herausgefunden haben, ruft eine linke äußere Verknüpfung alle Datensätze in der linken Tabelle ab, selbst wenn es keine zugehörigen Datensätze in der rechten Tabelle gibt. Deshalb erhalten wir zwar alle Computer, aber von der rechten Tabelle (DiskDrives) nur Datensätze, die einem Computer zugeordnet sind.

Alternativ kann beispielsweise eine Liste aller Laufwerke abgerufen werden, einschließlich derer, die nicht in einem Computer installiert sind. Weil die Tabelle „DiskDrives“ die rechte Tabelle in der Beziehung ist, wird die Syntax „RIGHT OUTER JOIN“ verwendet:

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

Von Rechts wegen sollte die Tabelle „Computer“ statt als linke Tabelle als „Ehemanntabelle“ in der Beziehung bezeichnet werden. Wie einige Scripting Guys aus schmerzhafter Erfahrung wissen, ist der Ehemann nie die rechte Entität in einer Beziehung.

Mit der rechten äußeren Verknüpfung erhalten wir alle Datensätze in der rechten Tabelle (alle Laufwerke), aber nur Computer (Datensätze in der linken Tabelle), die einem Laufwerk zugeordnet sind.

Dies dürfte vielen im Augenblick nicht logisch erscheinen, aber wenn Sie damit ein wenig herumexperimentieren, werden Sie sehen, wie es funktioniert. Wir stellen hier eine Beispieldatenbank zum Ausprobieren bereit: microsoft.com/technet/scriptcenter/resources/tnmag/archive.mspx.

Es ist wichtig zu erwähnen, dass Sie bei jeder Verwendung einer äußeren Verknüpfung mindestens auch die Zeile „On Error Resume Next“ als erste Zeile in Ihr Skript aufnehmen sollten. Warum? Nun, angenommen, es wird eine linke äußere Verknüpfung verwendet. In diesem Fall ist es möglich, einige Computer zu erhalten, die keine Festplatten installiert haben. Das ist schön (schließlich wollen wir genau das erhalten), jedenfalls bis diese Codezeile erscheint, die den Laufwerkbuchstaben angibt:

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

Weil für diesen bestimmten Computer kein Laufwerkfeld existiert, wird das Skript jäh zum Stillstand kommen:

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

Implementieren wir dagegen „On Error Resume Next“, kann das Skript die Tatsache ignorieren, dass ein Computer keine Laufwerke hat, und fortfahren. Alternativ könnten Sie Code dieser Art einsetzen, um den Wert des Laufwerkfelds zu prüfen und die passende Aktion vorzunehmen:

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

Mit diesem Code wird geprüft, ob wir einen Null-Wert im Gegensatz zu einem eigentlichen Laufwerkbuchstaben erhalten. Wenn ja, bedeutet dies „Keine Laufwerke installiert“. Wird kein Null-Wert angezeigt wird, wird der Wert im Laufwerkfeld angegeben. Das Ergebnis wird, wie in Abbildung 4 gezeigt, ausgegeben.

Figure 4 Anzeigen geeigneter Ergebnisse

atl-ws-01
C:

atl-ws-01
D:

atl-ws-02
C:

atl-ws-02
D:

alt-ws-03
No disk drives installed.

Sehen wir uns, ohne zu viele Erklärungen, ein paar weitere Abfragen an. Hier zum Beispiel eine Abfrage mit innerer Verknüpfung, die eine Liste von Computern und deren Laufwerken ausgibt, sofern diese größer als 50 GB sind (es wird hier vorausgesetzt, dass Laufwerkgrößen in Gigabyte gespeichert sind):

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

Wie Sie sehen, haben wir nichts weiter gemacht, als eine Standard-WHERE-Klausel zur ursprünglichen Abfrage hinzuzufügen:

WHERE DriveSize > 50

Was ist, wenn wir nur Informationen zu Laufwerk E auf Computern haben möchten? Kein Problem, man muss einfach die passende WHERE-Klausel hinzufügen:

WHERE Drive = 'E:'

Und hier ist eine etwas kompliziertere Abfrage, die eine Zusammenstellung der Computer ausgibt, auf denen keine Laufwerke installiert sind:

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

Wie gesagt, dies ist ein wenig komplizierter, und da wir nicht mehr viel Platz haben, erklären wir jetzt nicht, wie es funktioniert. Aber es funktioniert, das ist am wichtigsten.

Nun, vielleicht am zweitwichtigsten. Wie wir schon mehrmals gezeigt haben, sind Beziehungen das, was wirklich wichtig ist. Was nicht bedeutet, dass Beziehungen nicht schief gehen können. Wie heißt es doch am Ende von Woody Allens „Annie Hall“: „Eine Beziehung ist wie ein Hai; sie muss sich ständig weiter bewegen, sonst stirbt sie. Was wir hier vor uns haben ist ein toter Hai“. Ach, wenn Woody doch etwas von inneren und äußeren Verknüpfungen gewusst hätte! Mit diesen Abfrageverfahren werden Beziehungen immer erfolgreich!

The Microsoft Scripting Guys arbeiten für (und sind Angestellte von) Microsoft. Wenn sie nicht gerade ihrem Hobby, dem Baseball (oder verschiedenen anderen Aktivitäten) nachgehen, betreiben sie das TechNet-Skriptcenter. Besuchen Sie es unter www.scriptingguys.com.

© 2008 Microsoft Corporation und CMP Media, LLC. Alle Rechte vorbehalten. Die nicht genehmigte teilweise oder vollständige Vervielfältigung ist nicht zulässig.