 |
| Wie kann ich SUB..ENDSUB -Schleifen optimal einsetzen? |  |
 |
 |
Grundsätzliches
Prinzipiell kann man
sagen, dass es zwei Möglichkeiten gibt, mit der tdbengine Daten zu
selektieren und auszuwerten / auszugeben.
Der eine Weg führt über Einträge in der Markierungsliste zu einer
Tabelle und der anschliessenden "Abarbeitung" aller markierten
Datensätze mittels einem WHILE oder REPEAT -Konstrukt in Kombination mit ReadRec() - NextRec() -Anweisungen.
Der andere Weg, der den ich hier etwas näher beschreiben möchte, macht sich die (kaum dokumentierte) SUB..ENDSUB -Schleife zu Nutze.
SUB .. ENDSUB
Versuchen wir gar nicht erst SUB..ENDSUB in Worte zu fassen. Betrachten wir lieber gleich ein Beispiel:
PROCEDURE Main
VAR dbADRESSEN : INTEGER = OpenDB("..,/database/adressen.dat","",0, 0) //Öffne Tabelle im Lese-Modus
CGIWriteLn("content-type: text/html")
CGIWriteLn("")
CGIWriteLn("Adressen<hr><ul>")
Access(dbADRESSEN, "adressen.id")
SUB _DBName(dbADRESSEN) CGIWriteLn("<li>"+ GetField(dbADRESSEN,
"Nachname")+", "+ GetField(dbADRESSEN, "Vorname") +"</li>")
ENDSUB
CGIWriteLn("</ul>")
CloseDB(dbADRESSEN)
ENDPROC
Ergebnis dieses kleinen Programms wäre (vorausgesetzt es gibt eine
Tabelle adressen.dat mit den Feldern "Nachname" und "Vorname") eine
einfache HTML-Liste mit allen Namen, die in der Tabelle eingetragen
wurden.
Was genau passiert nun in diesem Beispiel?
Zuerst wird der neu deklarierten Variablen dbADRESSEN das Tabellen-Handle zur adressen.dat zugewiesen, welches von OpenDB() zurückgegeben wird. Damit sind wir in der Lage auf die Tabelle über eben dieses Handle zuzugreifen.
Anschliessend wird der übliche HTTP- und HTML-Header ausgegeben. Erst jetzt wird es wirklich spannend.
Mit der Access()-Anweisung
teilen wir der tdbengine mit, dass wir wünschen die Datensätze nach dem
ID-Index sortiert auszugeben. Freilich setzt dies vorraus, dass ein
entsprechender Index zur Tabelle existiert. Nehmen wir an, dies wäre
der Fall, und der Aufbau dieses Index wäre "Nachname:10,Vorname:5". Wir
möchten also die Datenausgabe wie in einem Telefonbuch gestalten.
SUB _DBName(dbADRESSEN)
Hier wird eine SUB-Schleife
eingeleitet. Dieser Aufruf ist mit Sicherheit die ein oder andere
Erklärung schuldig - wir kommen dem aber gleich nach.
SUB leitet einen SUB..ENDSUB -Block ein, während ENDSUB diesen beendet. D.h. der eine exisistiert nie ohne den anderen. So weit, so klar.
Mit SUB startet eine Schleife über die aktuell im Zugriff befindlichen
Daten und zwar in der Reihenfolge, die dieser Zugriff vorgibt. In
unserem Beispiel also werden ALLE Einträge nach dem ID-Index
(aufsteigend) sortiert ausgegeben.
Der Underscore oder auch Unterstrich vor dem DBName()-Befehl bewirkt die dynamische Auswertung eines beliebigen Ausdrucks zur Laufzeit. Hier bedeutet dies folgendes:
Zur Compile-Zeit weiss die tdbengine nichts von einer Tabelle
adressen.dat - sie kennt lediglich eine Referenz darauf : dbADRESSEN.
Die SUB..ENDSUB-Schleife ist ein Urgestein noch aus Zeiten der DOS-TDB. Dort wurde SUB..ENDSUB wie folgt verwendet:
SUB ADRESSEN, $ADRESSEN.Name WIE "Me?er" Print $ADRESSEN.Name + ", "+ $ADRESSEN.Vorname
ENDSUB
Das ging damals problemlos, da, anders als heute mit der tdbengine,
alle Tabellen einer Datenbank während der gesamten Laufzeit geöffnet
und somit über globale Variablen (hier $ADRESSEN) erreichbar waren.
SUB..ENDSUB funktioniert also noch wie vor "Äonen von Jahren" und
bedarf daher einer Sonderbehandlung ersten Grades, ist dafür aber ein
sehr mächtiges Werkzeug.
Letztlich ist also die Zeile
SUB _DBName(dbADRESSEN)
für die tdbengine zur Laufzeit gleichbedeutend mit
SUB ADRESSEN
Nur beim Kompilieren hagelt es Fehler, wenn Sie letzteres angeben -
ADRESSEN stellt in diesem Moment einen "Unbekannten Bezeichner" dar.
Zurück zur SUB-Schleife:
SUB _DBName(dbADRESSEN) CGIWriteLn("<li>"+ GetField(dbADRESSEN,
"Nachname")+", "+ GetField(dbADRESSEN, "Vorname") +"</li>")
ENDSUB
Mit jedem Durchlauf setzt der Datensatz-Zeiger beim jeweils nächsten
Eintrag auf. Solange bis der letzte Datensatz durchlaufen wurde.
Das Alternativ-Konstrukt mit einer ReadRec() - NextRec()-Schleife sähe in etwa so aus:
VAR nRec : INTEGER
nRec := FirstRec(dbADRESSEN)
WHILE nRec DO ReadRec(dbADRESSEN,nRec) CGIWriteLn("<li>"+ GetField(dbADRESSEN,
"Nachname")+", "+ GetField(dbADRESSEN, "Vorname") +"</li>") nRec := NextRec(dbADRESSEN)
END
Bis hierhin spricht lediglich die geringfügig kleinere Anzahl an Quellcode-Zeilen für die Verwendung von SUB..ENDSUB.
Ich stelle hier die Bedingungen!
So wie im Beispiel oben
beschrieben ist das alles noch recht unspektakulär. Ein bisschen mehr
Würze bringt da sicherlich die Verwendung (dynamischer)
Selektionskriterien.
Eigentlich haben wir diese gerade eben schon vorgstellt.
SUB _DBName(dbADRESSEN)
ist eine solche dynamische Selektionsanweisung - eine die auf alle Datensätze der Tabelle adressen.dat zutrifft.
Wie wäre es, wenn unser Telefonbuch so viele Einträge enthielt, dass
man diese unmöglich auf einer einzigen HTML-Seite darstellen könnte
(oder eher "wollte")?
Wir bräuchten einen alphabetischen Index. Jeder Buchstabe des Alphabets
erhält eine eigene Seite, die Adressen werden somit alphabetisch
gruppiert ausgegeben.
Um nun lediglich die Einträge aller mit "H" im Nachnamen beginnenden Einträge zu ermitteln wäre folgende Code-Zeile ausreichend
SUB _DBName(dbADRESSEN) + ", $Nachname[1] = 'H'"
Jetzt kommts faustdick! Zum besseren Verständnis übersetzen wir diese Zeile mal in die Laufzeit-Variante:
SUB ADRESSEN, $Nachname[1] = 'H'
Mit ADRESSEN selektieren wir bekanntlich ALLE Datensätze aus der
Tabelle adressen.dat. Mit dem Komma (einem logischen UND) verknüpfen
wir diese Selektion mit der nächstfolgenden.
Über $Nachname greifen wir nun auf den Inhalt des Feldes "Nachname" zu
- bei jedem Datensatz. Nur wenn dessen erster Buchstabe (deswegen das
[1]) identisch mit dem 'H' ist, dann wird dieser innerhalb der
SUB..ENDSUB -Schleife weiterverarbeitet. Ansonsten springt die
tdbengine direkt zum nächsten Datensatz. Das Interessante hierbei ist
sicherlich, dass wir nicht mit GetField() arbeiten, sondern direkt die Feld-Notation (die eben schon in der DOS-TDB gültig war) verwenden können.
Intern bewirkt diese Art von Selektion einiges an Optimierung. Die
tdbengine versucht unter Berücksichtigung existierender Indizes den
optimalen "Selektionsweg" zu ermitteln - es geht um Sekundenbruchteile,
manchmal auch um mehr!
Zum Vergleich: Die ReadRec() - NextRec() - Variante würde wie folgt aussehen:
VAR nRec : INTEGER
nRec := FirstRec(dbADRESSEN)
WHILE nRec DO ReadRec(dbADRESSEN,nRec) IF GetField(dbADRESSEN, "Nachname")[1] = "H" THEN CGIWriteLn("<li>"+
GetField(dbADRESSEN, "Nachname")+", "+ GetField(dbADRESSEN, "Vorname")
+"</li>") END nRec := NextRec(dbADRESSEN)
END
Sie haben also gesehen, wie leicht es ist, die Gesamtmenge der
auszuwertenden Daten auf bestimmte Teilmengen zu beschränken. Es
braucht lediglich einen gültigen Selektionsstring.
Diesen können Sie übrigens auch problemlos in einer Variablen halten und übergeben
...
VAR cSel : STRING = DBName(dbADRESSEN) +", Geburtstag < 1.1.1970 UND Vorname WIE 'thomas'"
...
SUB _cSel
...
ENDSUB
...
Vergessen Sie nur nie den Unterstrich, um die dynamische Auswertung zu erzwingen, denn SUB will keinen String als Parameter, es will eine Selektion.
SUB by SUB
Wenn Sie es bis hier her
geschafft haben durchzuhalten, ohne gänzliche in Langeweile aufzugehen,
dann haben Sie sich wirklich etwas mehr Spannung verdient.
Was kann so ein SUB..ENDSUB -Dino noch, ausser der Reihe nach durch die Daten laufen?
Zum Beispiel kann man damit Daten aus mehreren zueinander in Relation stehenden Tabellen holen.
PROCEDURE Main
VAR dbABTEILUNG : INTEGER = OpenDB("..,/database/abteilung.dat")
VAR dbADRESSEN : INTEGER = OpenDB("..,/database/adressen.dat")
RELATION
CGIWriteLn("content-type: text/html")
CGIWriteLn("")
CGIWriteLn("Abteilungszugehörigkeit<hr><ul>")
PrimTable(dbABTEILUNG) //Nimm abteilung.dat als Haupttabelle
Access(dbABTEILUNG, "abteilung.id") //Sortiere über ID-Index (=alphabetisch nach Name)
SUB _DBName(dbABTEILUNG) CGIWriteLn( GetField(dbABTEILUNG, "Name") +":<br>")
SUB _DBName(dbADRESSEN) CGIWriteLn("<li>"+
GetField(dbADRESSEN, "Nachname")+", "+ GetField(dbADRESSEN, "Vorname")
+"</li>") ENDSUB CGIWriteLn("</ul>") ENDSUB
CGIWriteLn("")
CloseDB(dbADRESSEN)
CloseDB(dbABTEILUNG)
ENDPROC
Ein mögliches Ergebnis:
Buchhaltung:
- Bares, Bärbel
- Schein, Sabine
EDV:
- Bit, Birgit
- Platine,Peter
- Virus, Viktor
Was ist nun alles neu?
Zum einen wäre da die Tatsache, dass wir (aus wirklich leicht verständlichen Gründen) nicht mehr nur eine OpenDB()-Anweisung, sondern zwei in unserem Programm stehen haben.
Es sollen ja nun auch die Abteilungen mit abgefragt werden, und die
liegen, so wollen es die Normalisierungsregeln nunmal, in einer eigenen
Tabelle abteilung.dat parat.
Nach dem 2. OpenDB() folgt die Anweisung RELATION.
Ausserdem bezieht sich der Access() -Aufruf, welchem hier nun ein PrimTable() vorausgeht, nicht mehr auf die Tabelle adressen.dat, sondern auf abteilung.dat
Die Funktion PrimTable() teilt der tdbengine mit, welche Tabelle als Primärdatei behandelt werden soll. Verwenden Sie PrimTable() am besten immer, bevor Sie mit mehreren zueinander in Beziehung stehenden Tabellen arbeiten.
Unser SUB..ENDSUB sieht diesmal ein wenig anders aus:
Es handelt sich hierbei um zwei ineinander verschachtelte Schleifen.
Der äussere Durchlauf wird auf die Tabelle abteilung.dat angewandt. Die
innere Schleife kennen wir bereits zur Genüge - es ist die gleiche wie
im allerersten Beispiel.
Im Folgenden wollen wir nun versuchen die neuen Befehle und Konstruktionen genauer zu sezieren...
RELATION
Mit RELATIONweisen Sie die tdbengine an, die Regeln und Automatismen des
ADL-Systems auf alle zuvor geöffneten Tabellen anzuwenden. Sie bringen
die "wahllos" geöffneten Tabellen also zueinander in Bezug.
Das alles funktioniert jedoch nur, wenn Sie Ihren Tabellen je ein
AUTO-Feld spendieren. Dieses dient als Primärschlüssel der jeweiligen
Tabelle und als Fremdschlüssel aller anderen, zu dieser angekoppelten,
Tabellen.
Zum Ankoppeln wiederrum benötigen Sie noch die sog. LINK-Felder. Wie
gerne würde ich nun an dieser Stelle einen Link zu einer ausführlichen
Beschreibung des ADL-Systems (Automatic Data Link) angeben. Leider habe
ich keinen parat. Vielleicht die Idee für ein weiteres HOWTO?!
Für unser obiges Beispiel müssen wir also voraussetzen, dass die
Tabelle abteilung.dat ein Feld vom Typ AUTO hat. Nehmen wir an, es
heisst AutoID. Ausserdem braucht unsere adressen.dat eine Möglichkeit
die AutoID der Abteilung in der ein Mitarbeiter tätig ist, zu
speichern. Dafür verwenden wir in der adressen.dat ein LINK-Feld namens
LinkABTEILUNG
Diese Kombination von AUTO- und LINK-Feldern macht einen der Clous (da
wären ansonsten noch zu nennen: Grösse, Geschwindigkeit,
Volltextindizierung, Programmierbarkeit., ..) der tdbengine
aus - sie kümmert sich um die Beziehungen. Zumindest bei SUB..ENDSUB
-Schleifen (und noch in einigen anderen speziellen Fällen).
Autor: Thomas Friebel <tf@tdb.de>
|