Deutsch English
Blog
Home
Über tdbengine
Newsletter
Download
Helpware
Chat
Dokumentation
Einführungskurs
Befehlsreferenz
HOWTO - Wie kann ich...?
Schnipsel
Projekte
Links
Benchmarks
Bug Reporting
Supportanfrage
 
Home    Überblick    Suche    Impressum    Kontakt    Mitglieder
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>


tdbengine Anwendungen im Web:

Open-Source Web CMS


Open-Source Bug-Tracking


Free wiki hosting

Open-Source Wiki-System

Kostenloses Foren-Hosting

Diät mit tdbengine 8-)

tdbengine chat
irc.tdbengine.org
#tdbengine

   Copyright © 2003-2004 tdb Software Service GmbH
   Alle rechte vorbehalten. / All rights reserved
   Letzte Änderung: 04.05.2004


ranking-charts.de

Programmers Heaven - Where programmers go!