![]() Home Dokumentation |
Einführungskurs:
CGI-Programmierung mit der tdbengine
Die Lektionen des Kurses:
Die Rechte beim Öffnen
von Tabellen
In der letzten Lektion haben wir eine kleine Suchmaschine für eine Adressen-Datenbank gebaut. Dabei haben wir die wichtigsten Funktionen aus der Standardbibliothek zum Öffnen und Schliessen von Tabellen sowie den lesenden Zugriff auf einzelne Zeilen und Spalten kennengelernt. Diesmal geht es nun darum, Informationen in eine Tabelle zu übertragen. Zunächst wollen wir neue Datensätze in die Tabelle einfügen. Dabei müssen wir folgende Reihenfolge einhalten:
Die Rechte beim Öffnen von TabellenDie Funktion OPENDB haben wir bereits kennengelernt. Bislang haben wir Tabellen aber ausschliesslich zum Lesen geöffnet. Bei den schreibenden Zugriffen unterscheiden wir folgende Aktionen:
Entsprechend dieser Hierarchie werden die Tabellenrechte beim Öffnen kodiert: 1 = Anhängen neuer Datensätze ist erlaubt (append,
insert)
Der Wert 4 für das Lösch-Recht mag zunächst verwundern. Er erlaubt uns aber, beliebige Recht-Kombinationen einfach durch Addition der jeweiligen Kennzahlen anzugeben: 3 = Anhängen und Verändern erlaubt
Zunächst sollte man ein Tabelle immer nur mit den minimalen Rechten öffnen, also mit denjenigen, die man später unbedingt benötigt. In diesem Fall sorgt dann die tdbengine dafür, dass irgendwelche Programm-Fehler (und solche gibt es immer und immer und immer) nicht unbedingt einen Datenverlust zur Folge haben. Es mag bequem sein, eine Tabelle immer mit allen Rechten zu öffnen, verantwortungsvolles Programmieren ist das aber nicht. Die Funktion OPENDB erwartet die Rechte-Kennzahl als letzten
Parameter, nach Tabellenpfad, Passwort und Verschlüsselungscode.
Auf Passwort und Verschlüsselungscode wollen wir in diesem Kurs verzichten. Also geben wir hier einen Leerstring und 0 an. Zum Öffnen der Tabelle "database/adressen.dat" mit dem Recht zum Anhängen neuer Datensätze benötigen wir demnach folgenden Funktionsaufruf: db:=OPENDB('database/adressen.dat'‘,''‘,0,1) Hinweis: Es gibt noch ein weiteres Recht
(8 = Indizieren erlaubt), das wir hier jedoch nicht weiter behandeln.
Satzpuffer leerenDas Leeren des Satzpuffers erfolgt durch Aufruf der Funktion READREC mit der Satznummer 0.READREC(Tabelle : REAL; 0) Nach der Ausführung der Funktion dieser Funktion ist der Satzpuffer komplett geleert, d.h.
Felder füllenIn der letzten Lektion haben wir kurz den Zugriff auf die einzelnen Felder (Spalten) eines Datensatzes (Zeile) angesprochen:GETFIELD(Tabelle : REAL; Spalte : REAL|STRING) : STRING Hinzu kommt noch die Möglichkeit, binär auf numerische Feldinhalte zugreifen zu können: GETRFIELD(Tabelle : REAL; Spalte : REAL|STRING) : REAL Beispiel: VARDEF Vorname, Name : STRING
Zu diesen beiden Funktionen gibt es die entsprechenden Gegenstücke für den umgekehrten Informationsfluss: SETFIELD setzt eine Tabellenspalte auf einen Wert (allgemeine
Version)
SETFIELD(Tabelle : REAL; Spalte : REAL|STRING; Wert : STRING) : STRING Diese Funktion ist für alle Spaltentypen (mit Ausnahme von Blobs und Memos) einsetzbar. Der Wert der Spalte wird immer als Zeichenkette angegeben. Beispiel: SETFIELD(db,'Name','Müller')
Der übergebene Wert wird bei der Ausführung der Funktion in den jeweiligen Spaltentyp konvertiert. Wenn dabei etwas schiefgeht, wird ein Laufzeitfehler ausgelöst (immer noch besser als Datenmüll). Diesen Laufzeitfehler können Sie mit den bekannten Methoden abfangen. Die aufwändige Konvertierung bei numerischen Feldern kann man durch den Einsatz von SETRFIELD umgehen: SETRFIELD(Tabelle : REAL; Spalte : REAL|STRING; Wert : REAL) : REAL Beispiel: SETRFIELD(db,'Aufnahmedatum',20.09.2000)
Bleiben noch längere Texte. Solche werden üblicherweise in Memos gespeichert. Dafür gibt es die Funktion READMEMO: READMEMO(Tabelle : REAL; Spalte : REAL|STRING; Textdatei : STRING) : REAL Doch die Zuweisung eines Textes an ein Memofeld ist eine wesentlich heiklere Operation. Das liegt daran, dass Memos in einer eigenen Datei abgelegt werden. In der eigentlichen Tabelle wird nur ein Zeiger (=die Position in der Zusatzdatei, an der das zugehörige Memo beginnt) gespeichert. Beim Einlesen ein Textes in ein Memofeld passiert also folgendes:
Das Hauptproblem beim Einlesen eines Textes in ein Memofeld besteht darin, dass die Operation einschliesslich des Schreibens des zugehörigen Datensatzes als eine Einheit durchgeführt werden soll. Andernfalls könnten inkosistente Zustände auftreten: In der Zusatzdatei ist der neue Text bereits eingetragen, aber im Memofeld steht noch der Verweis auf die Position des inzwischen gelöschten Textes... Aus dem genannten Grund führt die Funktion READMEMO eine komplette Transaktion (= Überführung eines Datenbanksystems von einem konsistenten Zustand in einer anderen) durch, der zugehörige Datensatz wird also gespeichert, zusätzlich zum Einlesen des Textes. Damit unterscheidet sich ReadMemo von den anderen Feld-Zuweisungen SETFIELD und SETRFIELD, denn hier wird nur der interne Satzpuffer verändert. Man wird also normalerweise bei der Anlage eines neuen Datensatzes mit Memos so vorgehen:
Datensatz in Tabelle anfügenDie Übertragung des Satzpuffers in die Tabelle erfolgt mit der Funktion WRITEREC:WRITEREC(Tabelle : REAL; Satznummer : REAL) : REAL Zulässige Werte für die Satznummer sind 1 bis
FILESIZE(Tabelle)+1. Ist der Wert kleiner als FILESIZE(Tabelle)+1, so wird
ein bestehender Satz überschrieben, andernfalls wird der Satz an die
bisherige Tabelle angehängt, sie wächst also um genau eine Zeile.
Informationen aus HTML-DokumentenSo, jetzt wissen wir in etwa, wie Felder belegt und Datensätze in die Tabelle eingefügt werden. Doch woher kommen die Informationen für die Felder? Selbstverständlich aus HTML-Formularen!Auch hier müssen wir wieder zwischen normalen"Feldern und Memos unterscheiden. Für die Eingabe von Memos kommt eigentlich nur das HTML-Tag "<textarea>" in Frage. Zwar könnte ein Tag der Form "<input type="text"> auch eine beliebig lange Zeile aufnehmen, aber halt nur eine Zeile. Für die anderen Felder gibt es ein ganze Reihe von
Eingabemöglichkeiten:
Jedes dieser Tags hat ein Option »name«.
Obwohl nicht unbedingt notwendig, spricht doch vieles dafür, hier
die gleichen Namen wie die entsprechenden Feldbezeichner zu wählen:
Am meisten werden wird wohl mit Textfeldern arbeiten. Hier gibt es zwei wichtige Optionen: size=»Breite des Eingabefeldes in Zeichen«
Da die Kapazität von Stringfeldern in einer Tabelle beschränkt ist, sollten Sie die Option »maxlength« immer entsprechend setzen. Andernfalls wird der Anwender häufig zu viele Zeichen eingeben, die dann nicht mehr in der Tabelle gespeichert werden können. Eine weitere wichtige Option, die alle Eingabe-Tags (mit Ausnahme von <textarea>) beinhalten, ist »value«. Dabei ist aber zwischen dem Textfeld <input type="text"...> und allen anderen Eingabefeldern zu unterscheiden.
Etwas anders liegt der Fall bei einem select-Tag. Dieser
bildet einen Container für einzelne option-Tags. Und innerhalb jedes
option-Tags kann des Wort »selected« eingesetzt werden,
um genau diese Option als vor-ausgewählt zu kennzeichnen.
Formular-TemplatesFür den Datenfluss zwischen Browser und CGI-Programm spielen nur diese Eingabefelder im Zusamenhang mit dem FORM-Tag eine Rolle, alle weiteren Gestaltungsmittel jedoch nicht. Aus diesem Grund ist es immer am Besten, mit einem möglichst schlichten Formular zu beginnen - ohne Tabellen oder sonstigen Schnickschnack.Ein solches Einfachstformular könnte für unsere
Adress-Datenbank beispielsweise so aussehen:
Hinweis: Es ist wenig sinnvoll, ein Eingabeformular direkt aus einem CGI-Programm heraus zu gestalten, denn dann müsste bei jedem Änderungswunsch am Design das Programm umgeschrieben werden. Zudem beraubt man sich damit der Möglichkeit, die Gestaltung einem Spezialisten zu übertragen, der ein Template durchaus mit seinen Methoden und Programmen (Dreamweaver, Frontpage etc.) bearbeiten kann. Sie könnten nun fragen, welchen Zweck die Platzhalter
für die einzelnen Value-Optionen erfüllen sollen, denn wir wollen
doch neue Datensätze aufnehmen, und dann sollten ja gerade keine Werte
an dieser Stelle stehen. Die Antwort darauf ist relativ einfach: Zunächst
kann das gleiche Template zur Bearbeitung bestehener Daten verwendet werden
(weise Voraussicht). Aber auch bei einer Neueingabe sind Fehleingaben durch
den Benutzer möglich. Und in diesem Fall wollen wir diesem das Formular
zur Korrektur zurückschicken (was auch den Platzhalter #fehlermeldung#
erklären dürfte). Dabei sollen natürlich seine bisherigen
Eingaben erhalten bleiben, und deshalb die Platzhalter für die einzelnen
Values.
Programmskizze für NeueingabeWir können nun unser Programm für die Neueingabe eines Datensatzes so skizzieren:
Um die Angaben aus dem Formular zwischenzuspeichern, legen wir dafür in unserem Programm globale Variablen an: VARDEF Name, Vorname, Strasse, PLZ, Ort, Telefon, EMail : STRING Die Prozedur zum Abholen der Formular-Angaben sollte uns
auch keine Schwierigkeiten mehr bereiten:
Die (Funktions-)Prozedur zur Überprüfung der
Eingabe gestalten wir so, dass sie gleich eine entsprechende Fehlermeldung
liefert. Wird keine Fehlermeldung geliefert, sind alle Angaben akzeptiert.
Während wir hier die Angaben zu Name, Vorname, Strasse, PLZ und Ort nur auf bloßes Vorhandensein prüfen, unterziehen wir die EMail einer genaueren Untersuchung. Die Standard-Funktion SCAN zählt die Vorkommen eines Substrings in einem String. Da in einer Email-Adresse das Zeichen '@' genau einmal vorkommen muss, können wir im gegenteiligen Fall die Angabe abweisen. Der Ausruck EMail like '?*@?*' sorgt dafür dass vor und nach dem '@'-Zeichen wenigstens ein Zeichen steht. Für Internet-Mail-Adressen könnten wir das Prüfmuster auf '?*@?*.?*' erweitern, denn hier besteht eine Domain immer aus Domain und Top-Level-Domain (wie »tdb-engine.de«). Im Intranet sind aber auch Rechnernamen ohne Punkt möglich. Als nächsten wollen wir uns um die Prozedur zum Anfügen
eines neuen Datensatzes kümmern.
Bleibt noch die Prozedur zur Wiedervorlage des Formulars,
wobei die auszugebende Fehlermeldung als Parameter übergeben wird.
Entsprechend unserer Programm-Skizze können wir jetzt
die Hauptprozedur Main schreiben:
Dieses Programm funktioniert schon recht gut. Es hat aber den kleinen Schönheitsfehler, dass doppelte Einträge nicht abgefangen werden. Doch wann ist ein Eintrag als doppelt zu betrachen? Wir wollen dieses Kriterium an der E-Mail-Adresse festmachen. Das Werkzeug, um zu prüfen, ob ein Eintrag bereits
vorhanden ist, haben wir bereits in der letzten Lektion kennengelernt.
Es versteckt sich hinter der Funktion »FindAndMark«.
Um diese Funktion anwenden zu können, muss die Tabelle geöffnet
sein. Deshalb wollen wir diese Prüfung in die Prozedur »DatensatzAnfügen«
einbauen:
Direkte FeldzugriffeEin Hinweis zur Syntax der Selektion in FindAndMark:
(Vergleicht das Feld »Email« aus der Tabelle »adressen« mit dem Inhalt der Variablen »Email«) Das schaut erst einmal komisch aus. In dieser Selektion
verwenden wir einen sogenannten direkten Feldzugriff:
Das ist eine erlaubte Abkürzung für
Die allgemeine Form für einen direkten Feldzugriff
lautet:
oder
Wenn die Eindeutigkeit sichergestellt ist, kann sowohl das $-Zeichen als auch der Tabellenname (mit dem folgenden Punkt) weggelassen werden. Da wir eine Variable mit dem Namen »Email« haben, ist zumindest das $-Zeichen notwendig. Der Einsatz von direkten Feldzugriffen setzt voraus, dass die zugehörige Tabelle geöffnet ist. Da die tdbengine (anders als TDB oder VDP) die Tabellen immer erst zur Laufzeit (also wenn das Programm ausgführt wird) öffnet, können im normalen Programtext keine direkten Feldzugriffe Erfolgen. Eine Anweisung wie: CGIWriteLn($adressen.Email) wird immer zu einer Fehlermeldung des Compilers führen. Sie können direkte Feldzugriffe ausschließlich in sogeannten dynamischen Argumenten verwenden, also solchen, die ebenfalls erst zur Laufzeit berechnet werden. Die Funktion FindAndMark akzeptiert in der Selektion ein solches dynamisches Argument. Aber warum, so werden Sie jetzt sicher fragen, soll man überhaupt einen direkten Feldzugriff verwenden? Die Selektion könnte ja auch so formuliert werden: 'getfield(db,"Email") like Email' Das ist richtig, und das funktioniert auch. Der Grund für den Einsatz: Die tdbengine kann direkte Feldzugriffe in Datenbank-Selektionen wesentlich effizienter verarbeiten. Bei kleinen Datenbanken mit wenigen tausend Datensätzen werden Sie keinen großen Unterschied feststellen. Wächst die Datenbank jedoch an, so wird die Suchstrategie entscheidend, und die kann (zumindest derzeit) nur dann automatisch optimiert werden, wenn direkte Feldzugriffe verwendet werden. Verbesserung des ProgrammsUnd noch ein kleiner Schönheitsfehler könnte uns die Freude an diesem Programm verdrießen: Beim ersten Aufruf über ».../cgi-tdb/adressengabe.prg« kommt gleich die Fehlermeldung: »Bitte Namen eingeben«, und nicht etwa ein Hinweis: »Bitte geben Sie hier Ihre Adresse und Ihre E-Mail ein«.Doch woher weiss das Programm, dass es über eine
gestartet (oder einen Link) gestartet wurde und nicht über das Formular?
Nun - zum Versenden der Formulars muss der »submit«-Schalter
gedrückt werden, und dieser kann mit CGIGetParam abgefragt werden.
Wie wir aus unserem Template ersehen können, hat dieser Schalter hier
den Namen »send«. Somit lautet der Test, ob der Schalter gedrückt
wurde:
Bauen wir diese Prüfung noch in unsere Hauptprozedur
ein:
Änderung bestehender DatensätzeZum Abschluss dieser Lektion wollen wir noch an die Änderung bestehender Datensätze wagen. Es könnte ja sein, dass jemand umzieht oder seine E-Mail ändert.Wiederum wollem wir an der E-Mail-Adresse die Identität festmachen (das Feld EMail hat in unserer Datenbank demnach die Funktion eines Primärschlüssels). Wir benötigen diesmal erst nur ein kleines Eingabeformular,
in das eine E-Mail-Adresse eingetragen werden kann und das unser Editierprogramm
startet. Der submit-Schalter sollte hier natürlich nicht den Namen
»send« haben, damit wir ihn einfach vom submit-Schalter unseres
Adressen-Formulars unterscheiden können. Wir wählen den Namen
»edit«:
Speichern Sie dieses Formular unter »templates/adressen_edit.html«. Das Editierprogramm kann dann so skizziert werden:
Die erste Abfrage »FALLS Email<>''« sorgt zum Einen dafür, dass wir in unserer Datenbank nicht nach leeren Einträgen suchen müssen, und zum Anderen, dass wir das Programm wiederum per Link oder URL-Eingabe starten können, denn hier kommt dann »Eingabe_wiederholen« zum Tragen. Zur Speicherung der gefundenen Informationen verwenden
wir wieder die gleichen Variablen wie bei der Neuaufnahme:
Zusätzlich benötigen wir noch eine Variable
für die Satznummer des zu bearbeitenden Datensatzes.
Da »HoleEmail« nur aus der einzelnen Anweisung »Email:=CGIGetParam('Email')« besteht, wollen wir hier auf eine eigenen Prozedur verzichten und die Anweisung selbst in die Hauptprozedur aufnehmen. Kommen wir zur Funktionsprozedur »Email_in_Datenbank«.
Im positiven Fall, also wenn wir die Email finden, soll diese Prozedur
auch gleich die Variablen »Name«, »Vorname« etc.
setzen, denn schließlich haben wir in dieser Prozedur die Tabelle
geöffnet.
Satznummer übergebenDie Prozedur Wiedervorlage können wir fast einszueins aus dem obigen Programm übernehmen. Wir müssen uns nur eine Möglichkeit überlegen, wie wir die Satznummer des gefundenen Datensatzes übergeben können, so dass die Prozedur »Aktualisere_Datensatz« später darauf zugreifen kann. Eine Möglichkeit bestünde darin, das zugehörige Template um ein entsprechendes Feld zu erweitern. Meist versteckt man innerhalb eines form-Tags solche Angaben in einem hidden-input-Feld (<input type=«hidden«...>). Die andere Möglichkeit besteht darin, die zusätzliche Information in den Programmaufruf (also die action-URL) zu verpacken. Das bewerkstelligen wir, indem wirsubst('#action#',paramstr(0)) ersetzen durch subst('#action#',paramstr(0)+'?recno='+Satznummer) Bekanntermaßen kann dann die Prozedur Aktualisere_Datensatz
mit der Funktion »GetQueryString« auf diesen Parameter zugreifen.
In dieser Prozedur wird auf »HoleEingaben« zurückgegriffen, die aus dem obigen Programm entnommen werden kann. Bleibt noch die Hauptprozedur:
In der nächsten (und abschließenden) Lektion
dieses Einführungskurses werden wird die einzelnen Programmteile zu
einer kompletten Internet-Datenbank zusammenführen.
Aufgabe:Ändern Sie das Programm zum Ändern bestehender Datensätze so ab, dass auch hier Fehleingaben überprüft werden. |