![]() Home Dokumentation |
Einführungskurs:
CGI-Programmierung mit der tdbengine
Die Lektionen des Kurses:
Terminologie
Nachdem wir nun die Grundzüge der Programmierung
in EASY und die wichtigsten allgemeinen Funktionen aus der Standardbibliothek
kennengelernt haben, geht es diesmal um die Arbeit mit Datenbanken.
TerminologieDie tdbengine bietet Datenbanken nach dem relationalen Datenbankmodell. Informationen werden dabei in einfachen Tabellen mit Zeilen und Spalten gespeichert. Die Spalten werden auch als (Tabellen-)Felder bezeichnet, die Zeilen als Datensätze. Der Zugriff auf einzelne Spalten erfolgt über Namen, die als Feldbezeichner oder (Feld-)Label bezeichnet werden. Der Zugriff auf die einzelnen Zeilen einer Tabelle erfolgt entweder über eine Satznummer (die Position des Satzes innerhalb der einer Tabelle zugrunde Datei) oder der Angabe einer Ausprägung eines bestimmten Feldinhaltes. Die Anordnung der Spalten zusammen mit deren Typisierung (= welcher Inhalt wie Zeichenette, Zahl, Datum etc. kann aufgenommen werden) bildet die Struktur einer Tabelle.Mehrere Tabellen können miteinander verknüpft werden, indem die Inhalte eines Feldes oder mehrerer Felder der einen Tabelle in eine Abhängigkeit zu entsprechenden Feldern der anderen Tabelle gesetzt werden. Mengenlogisch gesehen handelt es sich dabei um Teilmengen aus dem Kreuzprodukt der beiden Tabellen, also um Relationen. Eine oder mehrere Tabellen bilden eine Datenbank. Nach dieser recht trockenen Einführung wollen wir die Begriffe an einem Beispiel ansehen. Wir wollen dazu die klassische Firmendatenbank verwenden, weil sie jeder kennt und wahrscheinlich auch verwenden kann. Wir wollen das Beispiel insoweit vereinfachen, als wir nur Firmen mit einem Firmensitz betrachten. Unsere Datenbank soll also solche Firmen speichern, sowie zu jeder Firma eine beliebige Anzahl von Kontaktpersonen. Um dem zweiten Kriterium »beliebige Anzahl von Kontaktpersonen« gerecht werden zu können, benötigen wir zwei Tabellen: Firmendaten und Kontaktpersonen. Im nächsten Schritt gilt es, die Strukturen der Tabellen festzulegen, also die Spalten genau zu definieren: Firmen:
Kontaktpersonen:
Jetzt kommen wir zu einem typischen Problem relationaler Datenbanken: Die Tabelle »Kontaktpersonen« benötigt ein Feld, in dem gespeichert wird, zu welcher Firma eine Kontaktperson gehört (eine sogenannte Referenz auf die Firmen-Tabelle). Der erste Gedanke, hier einfach den Firmennamen mit aufzunehmen, erweist sich auf den zweiten Blick als nicht optimal: Können wir ausschließen, dass es nicht zwei Firmen mit absolut gleichem Namen gibt? Sie werden sagen, dass das sehr unwahrscheinlich ist. Aber Datenbanken benötigen keine Wahrscheinlichkeiten, sondern Sicherheit. Nun könnte man vielleicht auf die Idee verfallen, sämtliche Felder der Firmen-Tabelle in die Tabelle für die Kontaktpersonen zu übernehmen. Damit würden freilich viel zu viele redundante Informationen gespeichert, und Redundanzen sollten (im Sinne einer einfachen und effektiven Datenhaltung) auf einem sinnvollen Niveau bleiben. Die systematische Entfernung von Redundanzen aus einem Datenbanksystem durch Bildung von Relationen nennt man übrigens »Normalisieren« Als Anker für eine Referenz bietet sich also nur ein Feld an, das einen Datensatz (eine Zeile) innerhalb einer Tabelle eindeutig zuordnet. Ein solches Feld hat unsere Firmen-Tabelle (noch) nicht. Wir müssen also deren Struktur um ein solches Feld erweitern. Wenn es darum geht, Eindeutigkeit zu schaffen, bietet sich sofort eine Nummerierung an. Und damit wir nicht selbst jedes mal eine Nummer vergeben müssen (inkl. Prüfung auf Eindeutigkeit, die besonders schwierig ist, wenn im Netzwerkbetrieb mehrere Anwender gleichzeitig mit der Tabelle arbeiten), überlassen wir diese Arbeit dem Datenbank-System. Nahezu jede Datenbank bietet eine solchen Feldtyp an, der meist als AUTO-INCREMENT bezeichnet wird. Es liefert ein Feld, das die eindeutige Identifizierung eines Satzes erlaubt. Solche Felder werden als (Primär-)Schlüsselfelder bezeichnet. Felder, die eine Referenz auf einen Primärschlüssel einer anderen Tabelle speichern, werden auch als Fremdschlüssel bezeichnet. Die Tabelle für die Kontaktpersonen enthält dann demnach noch ein Feld, in dem der Firmenschlüssel gespeichert wird, wodurch die eindeutige Zuordnung einer Kontaktperson zu einer Firma hergestellt werden kann. Die tdbengine speichert AUTO-INCREMENT-Felder in einer 32-Bit-Zahl (NUMBER,4) ab, so dass wir diesen Typ auch für die Refernz verwenden müssen: Firmen:
Kontaktpersonen:
Das ADL-SystemNachdem eine solche Tabellenverknüpfung (eine referenzielle Zuordnung einer Tabelle zu einem Schlüssel einer anderen) so häufig verwendet wird, bietet die tdbengine für genau diesen Fall eine ganz wesentliche Vereinfachung an: den Feldtyp »LINK«. In einem Fortgeschrittenen-Kurs werden wir das ADL(=Automatic Data Link)-System der tdbengine genauer vorstellen und diskutieren.Die Strukturdefinition der TabellenWir können nun die Strukturen der beiden Tabellen als Strukturdefinition im tdbengine-Stil angeben:Firmen:
Kontaktpersonen:
IndizesWenn Sie die Firmenstruktur in unser »Database Developement Kit« eingeben und die Tabelle »Fimen« generieren wollen, erhalten sie die Fehlermeldung »kein ID-Index festgelegt«. Das wirft gleich zwei Fragen auf: Was ist ein Index im Allgemeinen? Und was ist ein ID-Index im Speziellen?Bevor wir die Frage beantworten, was in Index ist, wollen seine Funktion in einem Datenbanksystem beschreiben: Ein Index erlaubt einen sehr schnellen Zugriff auf einzelne Datensätze einer Tabelle bezüglich ganz bestimmter Felder. Dazu werden die Inhalte dieser Felder aus der Tabelle extrahiert und zusammen mit einer Referenz auf den zugehörigen Datensatz in einer speziellen Suchstruktur zur Verfügung gestellt. Das klingt kompliziert und ist in der technischen Ausführung noch viel komplizierter. Doch schauen wir zunächst wieder ein Beispiel an. Angenommen, unsere Firmentabelle enthält nun viele Tausend Einträge (Zeilen, Datensätze). Wir wollen nun die Firma mit dem Firmennamen »TDB GmbH« suchen (Datenbank-Deutsch: selektieren). Dazu können wir Datensatz für Datensatz lesen und prüfen, ob im Feld »Firmenname« die Zeichenkette »TDB GmbH« steht. Eine solche Suche wird als »sequentiell« bezeichnet. Leider dauert das Lesen der Datensätze und deren Überprüfung immer eine gewisse Zeit, so dass die Suche nicht besonders effizient ist und zudem die Dauer linear mit der Größe der Tabelle wächst. Wenn wir einen Index haben, der das Feld »Firmenname« berücksichtigt, so liefert uns der Index eine Referenz zu dem Datensatz, in dem die gewünschte Information steht, und wir müssen nur diesen lesen und nichts mehr prüfen. Damit ein Index auf diese Art und Weise effizient arbeiten kann, werden die extrahierten Informationen in ganz speziellen Suchstrukturen angelegt (meist baumartige Gebilde) die einen Zugriff erlauben, der nur noch logarithmisch mit der Tabellengröße wächst (grob gesagt ab einer bestimmten Tabellengröße nahezu konstant bleibt). Für Eingeweihte nur soviel: Die tdbengine legt einen Index als externen B-Tree an. Die tdbengine erlaubt in der derzeitigen Version die Anlage von bis zu 15 Indizes pro Tabelle. Einige Indizes werden automatisch angelegt, so beispielsweise ein Index über ein AUTO-INCREMENT-Feld. Weitere können jederzeit angelegt und wieder entfernt werden. Die Frage, was denn nun ein ID-Index ist, kann jetzt relativ einfach beantwortet werden: Ein ID-Index ist ein vom Anwender definierter Index, der den Hauptzugriff auf die Tabelle berücksichtigen soll, und der vom System eine besondere Pflege erfährt. Die besondere Pflege besteht darin, dass dieser Index automatisch immer wieder erzeugt wird, auch wenn die zugehörige Indexdatei (aus irgendeinem Grund) einmal verlorengeht. Eine weitere Rolle spielt der ID-Index im Zusammenhang mit dem bereits erwähnten ADL-System, das jedoch in diesem Kurs nicht besprochen wird. Nachdem der hauptsächliche Zugriff auf unsere Firmentabelle sicherlich über das Feld »Firmenname« erfolgt, werden wir dieses zum ID-Index verwenden. Die Indizes haben in der Strukturdefinition für die
tdbengine eine eigene Abteilung:
Der ID-Index wird hier folgendermaßen angeben:
Eine »Indexbeschreibung« ist wiederum eine
relativ komplexe Sache, weil die tdbengine nicht nur einfache Indizes über
ein Feld kennt, sondern zusätzlich hierarchische und berechnete Indizes
verarbeiten kann. Wir wollen hier nur den allereinfachsten Fall betrachten,
dass der Index über genau ein Feld angelegt wird. In diesem Fall besteht
die Indexbeschreibung genau aus dem entsprechenden Feldbezeichner:
Wir wollen hier also nochmals festhalten:
Datenbank-ZugriffWenn wir eine Anfrage an eine Datenbank stellen - etwa: »Gib mir alle Firmen aus München« - so gibt es für das Ergebnis zwei grundsätzlich verschiedene Strategien:
Im Normalfall müssen viel weniger Daten übertragen werden, denn die Zeilen werden werden nur bei echtem Bedarf vom DB-Server an den Klienten übertragen. Oftmals interessiert nicht die gesamte Ergebnistabelle, sondern nur ein fortlaufender Ausschnitt daraus, weil das Ergebnis beispielsweise seitenweise in einem Browser angezeigt wird. Zudem muss der DB-Server (also die tdbengine) in vielen Fällen die Tabelle nicht einmal lesen, um das Ergebnis bereitstellen zu können, weil beispielsweise die Informationen in einem Index ausreichen. Das dynamische Verfahren der tdbengine bietet die sogenannte Zugriffs-Aktualität. Ein Datensatz wird erst bei Bedarf vom Server aus der originalen Tabelle übertragen. In einer Datenbank, die häufigen Änderungen unterliegt (beispielsweise Zugriffszähler auf gut besuchte Web-Seiten) kann sich der Inhalt eines Datensatzes durchaus zwischen Selektion und Zugriff ändern. Somit steht eine möglichst hohe Aktualität zur Verfügung. Freilich bietet das System auch den Nachteil, dass der aktuell gelesene Satz die ursprüngliche Selektion nicht mehr erfüllt. Wenn solche Fälle ausgeschlossen werden müssen, kann das durch eine entsprechende Sperre der Tabelle realisiert werden. Die Klienten-Applikation kann recht geschmeidig auf ein Abfragergebnis reagieren. Es kann diese beispielsweise verwerfen, bevor auch nur ein Datensatz gelesen werden muss. Das ganze System arbeitet recht nahe an der Harware, mit allen Vor- und Nachteilen. Bei einem Abfrageergebnis handelt es sich um physikale Satznummern, also File-Positionen. Damit kann man unglaubliche Dinge tun - gute und schlechte. Der Hauptvorteil der Zugriffs-Strategie der tdbengine liegt darin, dass man mit ihr alle anderen »nachbauen« kann. So kann eine EASY-Prozedur geschrieben werden, die das Ergebnis ein Abrage in Form einer Ergebnistabelle zur Verfügung stellt. In einer der folgenden Versionen wird eine solche Prozedur auch in die Systembibliothek aufgenommen werden. PraxisNach so viel Theorie wollen wir nun die wichtigsten Funktionen der Standardbibliothek für Tabellen vorstellen.Da wären zunächst die Funktionen, die eine ganze
Tabelle betreffen:
Die ersten drei Parameter sind bei all diesen Funktionen (mit Ausnahme von CloseDB) gleich:
Ab dem vierten Parameter unterscheiden sich die Funktionen: MakeDB
(Kein weiterer Parameter) RenDB
Hinweis: Von MakeDB gibt es noch zwei Sonderformen: GenList und GenRel, die im Zusammenhang mit der Volltextindizierung die Arbeit vereinfachen. Öffnen und Schließen von TabellenBevor wir in einem EASY-Programm auf die Inhalte einer Tabelle zugreifen können, müssen wir diese öffnen. Dafür steht, wie bereits angemerkt, die Funktion OpenDB zur Verfügung:Die einfachste Form ist OpenDB(Pfad_zur_Tabelle : STRING) : REAL Das Ergebnis dieser Funktion ist entweder 0, wenn die Tabelle nicht geöffnet werden konnte, oder eine Zahl, die in der Folge die Verbindung zur Tabelle repräsentiert. Diese Zahl, den sogenannten »Tabellen-Handle«, speichern wir grundsätzlich in einer REAL-Variablen. Falls das Ergebnis 0 ist, wird ein Laufzeitfehler ausgelöst, der (wie in der letzten Folge dieses Kurses besprochen) abgefangen werden kann. Die Fehlerursache kann dann mit TDB_LastError etc. genauer untersucht werden (und steht auch in tdbengine/bin/error.log). Somit schaut die Standardsequenz in einem Programm so
aus:
Wie haben hier gleich CloseDB(db) verwendet. Diese Funkion schliesst eine Tabelle. Mit dieser einfachen Version von OpenDB wird eine Tabelle ohne Paswort und ohne Verschlüsselungscode nur zum Lesen geöffnet. Lesen von DatensätzenJeder Tabellenhandle verfügt über genau einen Satzpuffer, also einen Speicherbereich, der genau einen Datensatz aus der Tabelle speichern kann. Der Zugriff auf die einzelnen Felder erfolgt genau über diesen Satzpuffer. Doch wie gelangt nun der Inhalt einer Tabellenzeile in den Satzpuffer? Dafür gibt es die Funktion ReadRec:ReadRec(Tabellenhandle : REAL; Zeile : REAL) : REAL Diese Funktion überträgt (wenn alles gutgeht) die angegebene Zeile in den Satzpuffer. Das Funktionsergebnis ist entweder die Zeile (>=0) oder ein negativer Fehlercode. Auch hier wird ein Laufzeitfehler ausgeöst, wenn irgendetwas nicht richtig ist (weil zum Beispiel die angegebene Zeile garnicht existiert). Die Zeile bechreibt die physikalische Position innerhalb der Tabelle. Die größte mögliche Zahl für »Zeile« erhalten Sie mit der Funktion FileSize, die die Gesamtzahl der Zeilen in der Tabelle liefert: FileSize(Tabellenhandle : REAL) : REAL Wie wollen jetzt einmal ein kleiner Programfragment schreiben,
das eine komplette Tabelle durchgeht, also sämtliche Zeilen in den
Satzpuffer eines Tabelenhandle überträgt:
FeldzugriffeSchön, jetzt haben wir eine ganze Tabelle gelesen. Aber gesehen haben wir davon nichts. Wie bereits gesagt, überträgt ReadRec eine Zeile in den Satzpuffer. Auf den Satzpuffer können wir mit den sogenannten Feld-Funktionen zugreifen. EASY bietet u.a. folgende:GetField liefert den Inhalt eines eines Feldes aus dem
Satzpuffer
Der erste Parameter dieser Funktionen ist wiederum der Tabellenhandle. Der zweite Parameter kann entweder die Feldnummer (als Zahl) oder der Feldname (als String) sein. Bei SetField kommt als zusätzlicher Parameter der neue Wert des Feldes (als String) hinzu. Wie wollen zunächst nur GetField betrachten: GetField(Tabellenhandle : REAL; Feldnummer : REAL)
: STRING
Hinweis: Da sämtliche Feldfunktionen die Wahl zwischen Feldnummer und Bezeichner lassen, wollen wir in der Zukunft an dieser Stelle nur noch »Feld« schreiben. Der Zugriff über den Feldbezeichner ist zwar minimal langsamer als über die Feldnummer, liefert aber auch dann noch richtige Resultate, wen die Tabelle so umstrukturiert worden ist, dass sich die Feldnummern verändert haben. Deshalb wollen wir in diesem Kurs vorrangig mit Feldbezeichnern arbeiten. Zum letzten Mal erfolgt hier der Hinweis, dass auch diese Funktion (wie alle Tabellen- und Feld-Funktionen einen Laufzeitfehler auslösen, wenn illegale Parameter übergeben werden oder die Funktion aus anderen Gründen nicht ausgeführt werden konnte. Damit können wir schon unser erstes kleines CGI-Program
schreiben, das mit einer Tabelle arbeitet. Voraussetzung ist, dass Sie
die Übungen im zweiten Teil unseres Kurses durchgeführt haben
und somit die Tabelle »adressen.dat« im Verzeichnis »tdbengine/database«
(mit ein paar Einträgen) existiert. Andernfalls holen Sie das bitte
jetzt gleich nach, damit das Folgende so richtig Spaß macht.
Ein Tipp: Starten Sie das Program Developement Kit und übertragen Sie den Programmtext mittels Copy/Paste in das Text-Fenster. Den Programmnamen übernehmen Sie aus der MODULE-Zeile. So sparen Sie sich viel fehleranfällige Tipparbeit. Als nächstes wollen wie eine E-Mail-Liste aus unserer Adressdatenbank
erzeugen. Freilich sollen hier nur solche Sätze angezeigt werden,
wo auch eine E-Mail-Adresse eingetragen wurde.
MarkierungenNeben dem oben angesprochenen Satzpuffer bietet ein Tabellenhandle auch noch den Zugriff auf eine beliebig große Liste von Satznummern. In unserem Sprachgebrauch heisst diese Liste auch Markierungsliste. Folgende Basisfunktionen stehen für diese Liste zur Verfügung:
Es würde zu weit führen, alle diese Funktionen hier zu besprechen. Wir werden nur folgende genauer unter die Lupe nehmen:
FindAndMark(Tabellenhande : REAL; Selektion : STRING) : REAL Die Form einer Selektion (logischer Ausdruck) haben wir bereits im 5. Teil dieses Kurses besprochen. Die Arbeitsweise der Funktion ist recht schell erklärt: Nur diejenigen Sätze der Tabelle, die die Selektion erfüllen, werden in die Markierungsliste aufgenommen. Das Funktionergebnis ist die Anzahl der gefundenen Datensätze, bzw. ein negativer Fehlercode (beispielweise bei einer ungültigen Selektion). Somit reicht ein einfacher Aufruf von
um alle Münchener aus unserer Datenbak zu selektieren. Hinweis: Die Funktion hat noch einen optionalen dritten Parameter Multiexpresion (String), der für jeden gefundenen Datensatz berechnet wird. Das wird aber hier nicht weiter behandelt. Mit SortMark können wir die Ergebnisliste nach Feldinhalten sortieren: SortMark(Tabellenhandle : REAL; Indexbeschreibung : STRING) : REAL Wieder taucht hier der Begriff Indexbeschreibung auf.
Um die Sache nicht zu kompiliziert zu machen, geben Sie hier einfach die
Namen der Felder ein (durch Komma getrennt), nach denen sortiert werden
soll. Beispiel:
Die Funktion FirstMark liefert die erste Satznummer aus der Markierungslste, bzw. 0, wenn diese leer ist: FirstMark(TabellenHandle : REAL) : REAL NextMark hat zwei Parameter: NextMark(Tabellenhandle, Satznummer : REAL) : REAL Als Satznummer muss hier diejenige übergeben werden,
bzgl. der die nächste innerhalb der Markierungsliste gesucht wird.
Somit erhalten wird ein typisches Programmfragment für eine Datenbankabfrage
mit Selektion und Sortierung der Ergebnisse:
Adressen-SuchmaschineZum Abschluss dieser Lektion wollen wir eine kleine Adressen-Suchmaschine »basteln«. Typischerweise bestehen solche Suchmaschinen aus nur einem Formular, das im oberein Teil die Eingabe für die Suchbedingung enthält und das im unteren Teil die Treffer ausgibt. Wir vrwenden dazu folgendes Template:
Speicher Sie dieses Template unter »tdbengine/templates/adressensuche.html« ab. Unser Hauptprogramm kann recht einfach gehalten werden:
Die Prozedur »BearbeiteSuche« muss nun die übergebenen Werte aufbereiten. Zunächst werden die Platzhalter »#action#« und »#selection#« im Template ausgefüllt. Mit »#action#« steht der Platzhalter für
das CGI-Programm bereit, das die Formularauswertung vornimmt. Dieses ist
freilich auch das Profram, das das Formular zum Klienten schickt. Wir haben
es also mit einem Selbstaufruf zu tun. Die URL zum eigenen Programm erhalten
wir mit ParamStr(0).
Etwas anders verhält es sich mit dem Platzhalter
»#selection#«. Den Wert dafür erhalten wir aus dem Formular
selbst mit CgiGetParam('selection'). Da wir den Inhalt aber auch für
die Datenbankselektion selbst brauchen, wollen wir den Wert in einer Variablen
speichern:
Schön, bleibt nur der Platzhalter »#treffer#«. Aber der hat es dafür auch in sich! Am einfachsten liegt der Fall, wenn wir nichts finden,
denn dann reicht
Andernfalls gehen wir so vor: Für jeden gefundenen
Datensatz ersetzen wir »#treffer#« durch eine Ausgabe-Formatzeile,
gefolgt vom Wort »#treffer#«. Wir ersetzen also den Platzhalter
durch sich selbst (und damit rekursiv). Nachdem auf diese Weise alle gefundenen
Datensätze substituiert wurden, ersetzen wir »#treffer#«
durch einen Leerstring.
Der Gebrauch einer speziellen Formatzeile mag vielleicht etwas irritieren, sie bringt aber unglaubliche Flexibilität in unser Programm. Denn diese Zeile muss ja nicht unbedingt im Programm selbst definiert werden, sie kann beispielsweise auch aus einer Konfigurationsdatei stammen... Wir wollen hier noch einmal kurz darstellen, was bei der (rekursiven) Ersetzung von '#treffer#' passiert: (ursprünglich im Template:)
(nach SUBST('#treffer#',Formatzeile+'#treffer#'):)
(nach SUBST('#Vorname#;db,'Vorname') etc.:)
... Mit diesem Vorüberlegungen sollte unser Suchmaschine
leicht zu realisieren sein:
Sie sollten dieses kleine Programm in allen Einzelheiten verstehen. Es bildet die Grundage so vieler Internet-Anwendungen. In der nächsten Folge werden wir das Programm weiterentwickeln und um eine Eingabemöglichkeit ergänzen. Mit einer klitzekleinen Eweiterung erschließen wir unserer Suchmaschine noch wesentlich mehr Möglichkeiten: Ersetzen Sie hierzu die Zeile
durch
Das heisst, dass wir zuerst den Query-String (also einen via URL übergebenen Wert) abfragen, bevor wir den Wert aus dem Eingabefeld des Formulars holen. Damit wird die Suchmaschine auch über einen Link benutzbar. So können wie beispielsweise in ein HTML-Dokument folgendes einbauen: <a href="/cgi-tdb/adressensuch.prg?selection=Ort='Berlin'">
Dadurch wird ein Hyperlink erzeugt, und wenn ein Anwender auf diesen klickt, wird unsere Suchmaschine gestartet, wobei die Selektion Ort='Berlin' vorab ausgewählt ist und auch gleich bearbeitet wird. Genau nach dieser Methode arbeiten auch die großen Internet-Suchmaschinen, die neben dem eigentlichen Eingabefeld für Suchbegriffe meist eine Reihe von Links (mit vorbelegten Suchen) anbieten. Aufgaben:
|