Deutsch English
Blog
Home
Über tdbengine
Newsletter
Download
Helpware
Chat
Dokumentation
Einführungskurs
Grundlagen
Programmierumgebung
CGI Aufbereitung
EASY Programmierung
Standard-Bibliothek
Die Datenbank
HTML-Formulare
Befehlsreferenz
HOWTO - Wie kann ich...?
Projekte
Links
Benchmarks
Bug Reporting
Supportanfrage
 
Home    Überblick    Suche    Impressum    Kontakt    Mitglieder
Lektion 6: Die Datenbank der tdbengine
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.

Terminologie
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.

Ein Beispiel
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:

Firmenname Zeichenkette mit max. 60 Zeichen
Strasse Zeichenkette mit max. 40 Zeichen
Land Zeichenkette mit max. 10 Zeichen
PLZ Zeichenkette mit max. 20 Zeichen
Ort Zeichenkette mit max. 40 Zeichen
Telefon Zentrale Zeichenkette mit max. 20 Zeichen
Fax Zentrale Zeichenkette mit max. 20 Zeichen
Allgemeine E-Mail Zeichenkette mit max. 40 Zeichen

Kontaktpersonen:

Titel Zeichenkette mit max. 20 Zeichen
Vorname Zeichenkette mit max. 30 Zeichen
Name Zeichenkette mit max. 30 Zeichen
Position Zeichenkette mit max. 40 Zeichen
Funktion Zeichenkette mit max. 40 Zeichen
Telefon Zeicheknette mit max. 20 Zeichen
Telefax Zeichenkette mit max. 20 Zeichen
E-Mail Zeichenkette mit max. 40 Zeichen

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:
...
Firmenschlüssen (AUTO-INCREMENT)

Kontaktpersonen:
...
Firmenreferenz (32-Bit-Zahl)

Das ADL-System
Nachdem 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 Tabellen
Wir können nun die Strukturen der beiden Tabellen als Strukturdefinition im tdbengine-Stil angeben:

Firmen:

[STRUCTURE]
field_1=Firmenname,STRING,60
field_2=Strasse,STRING,40
field_3=Land,STRING,10
field_4=PLZ,STRING,20
field_5=Ort,STRING,10
field_6=Telefon,STRING,20
field_7=Telefax,STRING,20
field_8=EMail,STRING,40
field_9=Firmennummer,AUTO

Kontaktpersonen:

[STRUCTURE]
field_1=Titel,STRING,20
field_2=Vorname,STRING,30
field_3=Name,STRING,30
field_4=Position,STRING,40
field_5=Funktion,STRING,40
field_6=Telefon,STRING,20
field_7=Telefax,STRING,20
field_8=EMai,STRING,40
field_9=Firmenreferenz,NUMBER,4

Indizes
Wenn 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 ein Index ist, wollen wir 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:

[INDEX]

Der ID-Index wird hier folgendermaßen angeben:

id=Indexbeschreibung

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:

[INDEX]
id=Firmenname

Wir wollen hier also nochmals festhalten:

  • Ein Index wird über Spalten (Felder) einer Tabelle gebildet.
  • Er erlaubt eine besonders schnelle Suche nach diesen Spalten.
  • Zudem kann über diesen Index geordnet (sortiert) auf die Tabelle zugegriffen werden.
Datenbank-Zugriff
Wenn 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:

  • das Datenbank-System liefert eine komplette (Ergebnis-)Tabelle
  • das Datenbank-System liefert eine Menge von Verweisen auf Zeilen (Datensätze)

Die tdbengine arbeitet nach der zweiten Methode. Diese ist zwar komplizierter (vor allem bei der Bildung von sog. »Joins« - das sind Abfragen über mehrere verknüpfte Tabellen) und schwieriger zu verstehen, bietet aber auch einige Vorteile, die wir hier kurz anreissen wollen:

Performance

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.

Aktualität

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.

Flexibilität

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 Hardware, 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.

Praxis
Nach 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:

MakeDB() generiert eine neue Tabelle
DelDB() löschte eine bestehende Tabelle
RenDB() benennt eine bestehenede Tabelle um
OpenDB() öffnet eine bestehende Tabelle zur Bearbeitung
CloseDB() schließt eine geöffnete Tabelle

Die ersten drei Parameter sind bei all diesen Funktionen (mit Ausnahme von CloseDB) gleich:

  • Pfad zur Tabelle (Dateiname)
  • Passwort
  • Verschlüsselungscode
Hinweis: Ein Verschlüsselungscode wird nur aktiviert, wenn auch ein Passwort angegeben wird. Wir werden in diesem Kurs grundsätzlich mit ungeschützen Tabellen arbeiten, also weder Paswort noch Code angeben. Ab dem vierten Parameter unterscheiden sich die Funktionen:

MakeDB()
  • Pfad zu einer Strukturdefinition
  • Pfad zu einer Import-Quelle (optional)

DelDB (Kein weiterer Parameter)

RenDB

  • Neuer Name
OpenDB
  • Berechtigungen

Wir wollen die Funktionen MakeDB(), DelDB() und RenDB() hier nicht weiter besprechen, da Anlage und Umstrukturierung von Tabellen mit dem »Database Developement Kit« erledigt werden.

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 Tabellen
Bevor 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) : INTEGER

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 INTEGER-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:

VAR db : INTEGER
  SetPara('ec 1')
  db:=OpenDB('database/adressen.dat')
  IF db=0 THEN
    // Meldung, dass Tabelle NICHT verfügbar ist
  ELSE
    // hier kann mit der Tabelle gearbeitet werden
    CloseDB(db)
  END

Wie haben hier gleich CloseDB() 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ätzen
Jeder 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 : INTEGER; Zeile : INTEGER) : INTEGER

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 ausgelö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 : INTEGER) : INTEGER

Wir wollen jetzt einmal ein kleines Programmfragment schreiben, das eine komplette Tabelle durchgeht, also sämtliche Zeilen in den Satzpuffer eines Tabellenhandle überträgt:

VAR db, anzahl_zeilen, zeile : INTEGER
  db:=OpenDB('database/adressen.dat')
  anzahl_zeilen:=FileSize(db)
  zeile:=0 // eigentlich überflüssig, weil mit 0 initialisiert
  WHILE zeile:=zeile+1<=anzahl_zeilen DO //ODER zeile++<=anzahl_zeilen
    ReadRec(db,zeile)
  END

Feldzugriffe
Schö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() liefern den Inhalt eines eines Feldes aus dem Satzpuffer
SetField() schreibt einen String in ein Feld des Satzpuffers

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

GetField(Tabellenhandle : REAL; Feldbezeichner : STRING) : 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, wenn 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.

MODULE datenbanktest_1.mod
PROCEDURE Main
  VARDEF db, anzahl_zeilen, zeile : INTEGER
  CGIWriteLn('content-type: text/html')
  CGIWriteLn('')
  CGIWriteLn('<html><body bgcolor="white">')
  CGIWriteLn('<h2>Tabellen Auswertung</h2>')
  CGIWrite('<pre>')
  SetPara('ec 1')
  IF db:=OpenDB('database/adressen.dat')=0
  THEN CGIWriteLn('Tabelle adressen.dat NICHT gefunden.')
  ELSE
    anzahl_zeilen:=FileSize(db); zeile:=0
    WHILE zeile:=zeile+1<=anzahl_zeilen DO
      ReadRec(db,zeile)
      CGIWriteLn('Name: '+ToHtml(GetField(db,'Name')))
      CGIWriteLn('Vorname: '+ToHtml(GetField(db,'Vorname')))
      CGIWriteLn('Strasse: '+ToHtml(GetField(db,'Strasse')))
      CGIWriteLn('PLZ: '+ToHtml(GetField(db,'PLZ')))
      CGIWriteLn('Ort: '+ToHtml(GetField(db,'Ort')))
      CGIWriteLn('---------------')
    END
    CloseDB(db)
  END
  CGIWriteLn('</body></html>')
ENDPROC

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.

MODULE datenbanktest_2.mod
PROCEDURE Main
VARDEF db, anzahl_zeilen, zeile : INTEGER
VARDEF email : STRING
  CGIWriteLn('content-type: text/html')
  CGIWriteLn('')
  SetPara('ec 1')
  IF OpenDB('database/adressen.dat')=0
  THEN CGIWriteLn('Tabelle adressen.dat NICHT gefunden.')
  ELSE
    anzahl_zeilen:=FileSize(db); zeile:=0
    WHILE zeile:=zeile+1<=anzahl_zeilen DO
      ReadRec(db,zeile)
      IF email:=GetField(db,'Email') THEN
        CGIWrite('<a href="mailto:'+email+'">')
        CGIWrite(ToHTML(GetField(db,'Vorname')))
        CGIWrite(' ')
        CGIWrite(ToHTML(GetField(db,'Name')))
        CGIWriteLn('</a><br>')
      END
    END
    CloseDB(db)
  END
  CGIWriteLn('</body></html>')
ENDPROC

Markierungen
Neben 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:

SetMark() hängt eine Satznummer an das Ende der Liste
DelMark() löscht eine Satznumer aus der Liste
DelMarks() löscht die gesamte Liste
IsMark() prüft, ob eine Satznummer in der List ist
FirstMark() liefert den Anfang der Liste
NextMark() liefert die nächste Satznummer aus der Liste
Revmarks() invertiert die Markierungsliste
SortMark() sortiert die Markierungliste
FindAndMark() erzeugt eine Markierungsliste bzgl. einer Selektion
NMarks() liefert die Anzahl der Datensätze in der Markierungsliste

Es würde zu weit führen, alle diese Funktionen hier zu besprechen. Wir werden nur folgende genauer unter die Lupe nehmen:

FindAndMark() ist eine neue Funktion (erstmals in Version 6.2.4), die einen sehr schnellen Zugriff auf einzelne Tabellen erlaubt.
Hier die Syntax:

FindAndMark(Tabellenhande : INTEGER; Selektion : STRING) : INTEGER

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

FindAndMark(db,'Ort= "München"')

um alle Münchener aus unserer Datenbank zu selektieren.

Hinweis: Die Funktion hat noch einen optionalen dritten Parameter Multiexpression (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 : INTEGER; Indexbeschreibung : STRING) : INTEGER

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:

SortMark(db,'Name,Vorname')

Die Funktion FirstMark() liefert die erste Satznummer aus der Markierungsliste, bzw. 0, wenn diese leer ist:

FirstMark(TabellenHandle : INTEGER) : INTEGER

NextMark() hat zwei Parameter:

NextMark(Tabellenhandle, Satznummer : INTEGER) : INTEGER

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:

VAR db, zeile : INTEGER
VAR Selektion, Sortierung : STRING
...
Selektion:='Ort="München"'
IF FindAndMark(db, Selektion)<=0 THEN
  // Kein Treffer, eventuell TDB_LastError auswerten
ELSE
  Sortierung:='Name,Vorname'
  SortMark(db,Sortierung)
  zeile:=FirstMark(db)
  WHILE zeile>0 DO
    ReadRec(db,zeile)
    // Ausgabe der Felder ....
    zeile:=NextMark(db,zeile)
  END
END

Adressen-Suchmaschine
Zum 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:

<html>
<head>
  <title>Adressen-Suchmaschine</title>
</head>
<body bgcolor="white">
  <h2>Adressen-Suchmaschine</h2>
  <form action="#action#" method="post">
    <table border="1">
      <tr>
        <td>Suchbedingung: </td>
        <td><input type="text" name="selection" value="#selection#"></td>
        <td><input type="submit" name="submit" value="suchen"></td>
      </tr>
    </table>
    <hr>
    #treffer#
  </form>
</body>
</html>

Speicher Sie dieses Template unter »tdbengine/templates/adressensuche.html« ab.
Unser Hauptprogramm kann recht einfach gehalten werden:

MODULE adressensuche.mod

PROCEDURE Main
  CGIWriteLn('content-type: text/html')
  CGIWriteLn('')
  LoadTemplate('templates/adressensuche.html')
  BearbeiteSuche
  CGIWriteTemplate
ENDPROC

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 Programm, 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).

Subst('#action#',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:

VAR user_selection : STRING
  user_selection:=CGIGetParam('selection')
  Subst('#selection#',user_selection,1) // ...,1, damit auch Umlaute stimmen

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

SUBST('#treffer#','Leider nichts gefunden')

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.

Formatzeile:='#Vorname# #Name# #Strasse# #PLZ# #Ort#<br>'

Für jeden gefundenen Datensatz:

Subst('#treffer#',Formatzeile+'#treffer#')
Subst('#Vorname#',db,'Vorname',1)
Subst('#Name#',db,'Name',1)
... Subst('#treffer#','')

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:) #treffer#

( nach SUBST('#treffer#',Formatzeile+'#treffer#'):) #Vorname# #Name# #Strasse# #PLZ# #Ort#<br>#treffer#

(nach SUBST('#Vorname#;db,'Vorname') etc.:) Hans Müller Amselweg 12 80335 München<br>#treffer#

... Mit diesem Vorüberlegungen sollte unser Suchmaschine leicht zu realisieren sein:

MODULE adressensuche.mod

PROCEDURE BearbeiteSuche
VAR user_selection : STRING
VAR db, zeile : INTEGER
VAR Formatstring : STRING
  Subst('#action#',ParamStr(0))
  user_selection:=CGIGetParam('selection')
  Subst('#selection#',user_selection,1)
  SetPara('ec 1')
  IF db:=OpenDB('database/adressen.dat')=0 THEN
    Subst('#treffer#','Datenbank NICHT gefunden')
  ELSE
    IF FindAndMark(db,user_selection)=0 THEN
      Subst('#treffer#','Leider nichts gefunden')
    ELSE
      Formatstring:='#Name# #Vorname# #PLZ# #Ort#<br>'
      SortMark(db,'Name,Vorname')
      zeile:=FirstMark(db)
      WHILE zeile DO
        ReadRec(db,zeile)
        Subst('#treffer#',Formatstring+'#treffer#')
        Subst('#Name#',db,'Name',1)
        Subst('#Vorname#',db,'Vorname',1)
        Subst('#PLZ#',db,'PLZ',1)
        Subst('#Ort#',db,'Ort',1)
        zeile:=NextMark(db,zeile)
      END
      Subst('#treffer#','')
    END
    CloseDB(db)
  END
ENDPROC


PROCEDURE Main
  CGIWriteLn('content-type: text/html')
  CGIWriteLn('')
  LoadTemplate('templates/adressensuche.html')
  BearbeiteSuche
  CGIWriteTemplate
ENDPROC

Sie sollten dieses kleine Programm in allen Einzelheiten verstehen. Es bildet die Grundlage 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

user_selection:=CGIGetParam('selection')

durch

IF user_selection:=GetQueryString('selection')=''
THEN user_selection:=CGIGetParam('selection')
END

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'">Alle Adressen aus Berlin</a>

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:
  1. Ändern Sie unsere Suchmaschine so ab, dass auch das Sortierkriterium im Formular eingestellt werden kann.
  2. Bauen Sie eine Fehlerbehandlung ein, so dass bei einer ungültigen Selektion der Platzhalter #treffer# eine sinvolle Fehlermeldung aufnimmt.
  3. Ändern Sie die Suchmaschine so ab, dass nur ein Suchbegriff eingeben werden muss, und das Programm diesen Begriff automatisch in den Feldern Name, Vorname, Strasse und Ort sucht (Tipp: '*'+Suchbegriff+'*' IN [Name,Vorname...])


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: 05.05.2004


ranking-charts.de

Programmers Heaven - Where programmers go!