Perls generische Datenbankschnittstelle DBI
bietet einen
Treiber an, der SQL-Abfragen auf Datenbestände ohne eine
Datenbank zuläßt -- ordinäre Dateien speichern die Tabellendaten
in komma-separierten Einträgen. Ideal für ein kleines
CGI-Adreßbuch!
Notizbücher und Palmtops haben den Nachteil, daß man sie immer dann einzustecken vergißt, wenn man sie am nötigsten braucht. Wer wie ich eh den ganzen Tag am Internet hängt, kommt da schon mal auf die Idee, ein Web-basiertes Adreßbuch anzulegen!
Nun hat nicht jeder hat eine Datenbank zur Verfügung -- sei es, daß man aus purer Faulheit keine installieren will oder deshalb, weil der Internetprovider einfach keine anbietet. Andererseits sollte jede Applikation, die mit Daten jongliert, Datenbank-tauglich angelegt sein, man weiß schließlich nie, ob die Datenbestände nicht wider Erwarten doch plötzlich sprungartig wachsen und man schnellstens in die Arme von Mama Oracle oder einer Ihrer Kolleginnen springen möchte. Schön, wenn man dann nur eine Zeile im Skript ändern muß, die den richtigen Treiber installiert und die restliche Applikation mitsamt den SQL-Abfragen gleichbleibt.
Perls generische Datenbankschnittstelle DBI
kann man seit neuestem
auch mit einem Treiber für ordinäre Dateien ansteuern -- statt
mit einer Datenbank zu kommunizieren,
verwaltet das DBD::File
-Modul lesbare Dateien
mit Komma-separierten Einträgen, die man mit SQL-Zugriffen traktieren
darf. Hinweise zur Installation des Treibers und einiger abhängiger
Module, sowie zur Aktivierung des vorgestellten Skripts addr.pl
finden sich im Abschnitt ``Installation''.
Abbildung 1 zeigt das Eingangsformular, das das CGI-Skript addr.pl
,
wenn es einmal
in cgi-bin
installiert und initialisiert ist, beim ersten Aufruf
in den Browser zaubert: Zunächst zeigt addr.pl
keinerlei Daten an
(auch wenn die Tabelle schon mit Einträgen gefüllt wäre), sondern
nur eine Liste der Buchstaben des Alphabets, deren jeder mit einem
Link verknüpft ist, der addr.pl
nach einem Eintrag suchen läßt,
dessen Vorname oder Nachname mit dem betreffenden Buchstaben anfängt.
Ein Klick auf den Alle Einträge-Link zeigt das ganze Notizbuch an.
Weiter wird ein Suchfeld angezeigt, das Suchstrings entgegennimmt.
Falls Suche starten gedrückt wird, sucht addr.pl
ebenfalls
in den Vorname/Nachname-Feldern der Tabelle nach Übereinstimmungen
und zeigt die Ergebnisse in einer Liste an. Klickt der Benutzer auf
den Neuer Eintrag-Knopf, wird ein Formular nach Abbildung 2
angezeigt. Der nach dem Ausfüllen der Felder gedrückter
Speichern-Knopf läßt die Daten in die Datenbank wandern.
Die Einträge in der Adreßbuchdatei liegen bei dem im Skript
verwendeten DBD::File
-Treiber in der Datei addressbook/addressbook
unterhalb des CGI-Verzeichnisses, der Inhalt sieht etwa folgendermaßen
aus:
id,fname,lname,phone,email,addr,notes 9214031581423,Freddy,Holler,089/1234567,holler@aol.com, "Bon-Scott-Weg 3, 89834 Zuffenhausen", "Alte Email: fredy@aol.com" 9214038931493,Herbert,Rigatoni,08234/234435, herbertR@yahoo.com,"In der Grube 24, 82342 Kutzenbach", "Geburtstag: 1.3."
Die erste Zeile legt hierbei die Spaltennamen fest. Sie lauten genauso
wie die später verwendeten CGI-Parameternamen, das erspart Kopfschmerzen
bei der Programmierung. fname
steht dabei für den Vornamen
(First Name), lname
für den Nachnamen (Last Name) usw.
Einträge in den Datenzeilen werden durch Kommata getrennt. Einträge, die Leerzeichen enthalten, werden in doppelte Anführungszeichen eingeschlossen und eventuelle Sonderzeichen (doppelte Anführungszeichen und Kommata) entsprechend maskiert.
Die erste Spalte (id
) jeder Zeile weist dem Eintrag eine eindeutige
ID zu. Sie setzt sich zusammen aus der gegenwärtigen Uhrzeit (Rückgabewert
des time()
-Kommandos) und der PID des aktuellen CGI-Prozesses beim
Anlegen des Eintrags.
Eine gefüllte Datenbank liefert Ergebnisse auf Suchabfragen in Tabellenform nach Abbildung 3. Das Namensfeld jedes Eintrags ist mit einem Link unterlegt, klickt man darauf, springt der Browser auf die Editierseite und füllt die Felder dort gleich mit den Daten des ausgewählten Eintrags. Der Speichern-Knopf dort aktualisiert den Datenbank-Eintrag entsprechend den Formularfeldern, ein Druck auf den Delete-Knopf löscht den Eintrag.
Was addr.pl
im einzelnen tut, ob es ein Eingabeformular darstellt
oder einen neuen Eintrag anlegt oder das Ergebnis einer Suchanfrage
anzeigt, bestimmen die CGI-Parameter mit denen es aufgerufen wird.
Folgende Szenarien steuert addr.pl
:
addr.pl init=1
Dieser Aufruf erzeugt die Datenbank-Tabelle mit einem CREATE
-Kommando
aus dem SQL-Fundus.
addr.pl search=A
Die Such-Abfrage fördert Einträge hervor, deren Vorname oder Nachname
mit A
angehen und zeigt die Ergebnisse in einer Tabelle an. Wird der
search
-Parameter leergelassen (aber dennoch definiert mit search=
),
zeigt addr.pl
eine vollständige
Liste aller bestehenden Einträge in einer Tabelle an.
addr.pl edit=1
addr.pl edit=1 id=9214031581423
Jeder Tabelleneintrag enthält eine eindeutige ID, sodaß addr.pl
einmal gefundene Zeilen beim nächsten Aufruf schnell identifizieren
und Manipulationen vornehmen kann (update, delete).
addr.pl insert=<gesetzt> fname=... lname=...
addr.pl insert=<gesetzt> id=9214031581423 fname=... lname=...
addr.pl delete=<gesetzt> id=9214031581423
addr.pl
im DetailListing addr.pl
zeigt die Implementierung des Web-Adreßbuches.
Zeile 7 holt das CGI-Modul, die angegebenen Tags lassen es die
Standard-HTML- und die Tabellen-Funktionen exportieren.
Das CGI::Carp
-Modul sorgt dafür, daß der Browser bei auftretenden
Fehlern nicht das blöde Internal Server Error
anzeigt, sondern
einen aufschlußreiche Fehlermeldung.
Die Zeilen 11 bis 14 spezifizieren die Parameter für den
DBI
-Flatfile-Treiber. $DB_DIR
gibt das Verzeichnis unterhalb
des cgi-bin
-Verzeichnisses an, das die Tabellendaten als
Datei enthält.
Zeile 20 nimmt die Verbindung mit der 'virtuellen' Datenbank auf,
die Zeilen 23 und 24 geben den CGI-Header und die Überschrift aus,
die in jedem Fall im Dokument steht und färben den Hintergrund
der Seite weiß ein. Dann scheiden sich die Wege:
Der if
-Block ab Zeile 26 wird angesprungen, falls ein Benutzer
die Formularfelder für einen neuen Eintrag ausgefüllt und den
Speichern-Knopf gedrückt hat. Der map
-Befehl in Zeile 28
übergibt der insert_rec
-Funktion, die die eigentliche
Datenbank-Aktualisierung vornimmt, die Formulardaten, indem er
für alle Elemente in @dbfields
die param
-Funktion des
CGI-Moduls aufruft und so die entsprechenden CGI-Parameter
entgegennimmt und weiterreicht.
insert_rec
selbst steht ab Zeile 158 in addr.pl
, nimmt
das hereingereichete DB-Handle und die Formularparameter
entgegen und setzt den SQL-Insert/Update-Befehl an die Datenbank ab.
Ist der CGI-Parameter id
gesetzt, handelt es sich um eine
Aktualisierung eines bestehenden Records und Zeile 167 definiert
einen SQL-Update-Befehl. Hier wie auch an anderen Stellen leistet
der qq
-Operator, der mehrzeilige Strings mit doppelten Anführungszeichen
umschließt, nützliche Dienste.
Fehlt andererseits id
, handelt es sich um einen neuen Eintrag und
Zeile 180 kreiert einen SQL-Insert-Befehl.
Zurück zur Hauptschleife:
Die page_header
-Funktion, die in Zeile 29 aufgerufen wird
und ab Zeile 99 implementiert ist, klatscht das kleine
Link-Alphabet, das in den Abbildungen 1 und 3 jeweils oben im Fenster
zu sehen ist, dorthin und schreibt auch noch das Such-Feld
samt den zwei Buttons auf die Seite. Die url()
-Funktion
aus dem CGI-Modul liefert hierzu den URL des gegenwärtig
laufenden Skripts.
Ab Zeile 31 steht der Code zum Löschen eines Eintrags. Jede
Zeile in der Datenbanktabelle enthält neben den Adreßbuchdaten
auch noch eine eindeutige ID, die als verstecktes (hidden) Feld
auf der Seite steht, die
die Formularfelder zum Aktualisieren eines Eintrags darstellt.
Drückt der Benutzer auf den Knopf Eintrag löschen, sendet
der Browser neben den aktualisierten Feldern auch noch die ID
mit und addr.pl
kann einen DELETE-Befehl losschicken, der
mit seinem Tintenkiller genau über die richtige Zeile der
Tabelle fährt.
Drückte der Benutzer entweder auf den Knopf Neuer Eintrag
oder aber auf einen erleuchteten Namen der der Ergebnisliste,
wird der Codeblock ab Zeile 40 angesprungen, da der Parameter
edit
in diesen Fällen gesetzt ist.
Diese zwei Fälle unterscheiden sich dahingehend, daß ein
angeklickter Eintrag der Ergebnisliste den id
-Parameter
setzt. In diesem Fall muß addr.pl
vor dem Darstellen der
Felder die Werte aus der Datenbank übernehmen. Hierzu
erzeugt es in Zeile 44 einen SQL-Select-Befehl, der die Daten
aus der Datenbank holt. Die Zeilen 51 bis 56 holen das Ergebnis
des Queries ab, wegen der eindeutigen ID im SELECT ist das Ergebnis
stets eine einzelne Zeile. Die fetch
-Methode im while
-Kopf
liefert eine Referenz auf einen Array zurück, dessen Elemente die
Spaltenwerte des Tabelleneintrags beinhalten. Da die Tabellenzeile
zusätzlich zu den in @dbfields
aufgelisteten Spalten
als erstes Element die id
-Spalte führt, startet der Index $i
in
Zeile 52 mit dem Wert 1
statt des sonst üblichen 0
.
Der Aufruf der param
-Methode in Zeile 54 manipuliert die
CGI-Eingangsparameter und gaukelt den nachfolgenden Abfragen vor,
der Benutzer hätte die Adreßdaten des selektierten Eintrags selbst
eingetragen -- derweil stammen sie aus der Datenbank.
Die Zeilen 59 bis 80 geben eine zweispaltige HTML-Tabelle aus, die das Formular zum anlegen/editieren eines Adreßeintrags nach Abbildung 2 in den Browser zeichnet.
Für den Fall, daß der Benutzer eine Suchanfrage startete oder einen
Buchstaben im Reiter-Alphabet des Seitenkopfes anklickte, ist der
CGI-Parameter search
gesetzt, entsprechend springt addr.pl
den Block ab Zeile 82 an. Für den Buchstabenklick enthält search
den entsprechenden Buchstaben, wurde etwas ins Suchfeld eingetragen
und der Suche starten-Knopf gedrückt, steht in search
der Suchbegriff.
Die Funktion keyword_search
übernimmt in beiden Fällen die Suche,
sie ist ab Zeile 118 definiert. Dort holt eine SQL-Abfrage passende
Records aus der Datenbank, indem sie mittels des CLIKE-Konstrukts
überprüft, ob Vor- oder Nachname eines Eintrags mit dem gegebenen
Suchausdruck beginnen, Groß- und Kleinschreibung werden ignoriert.
Für einen leeren Suchstring liefert keyword_search
großzügigerweise einfach alle Einträge der Tabelle.
Die while
-Schleife ab Zeile 145 gibt die Treffer in einer
HTML-Tabelle aus, indem sie Vor- und Nachnamen zu einer Tabellenspalte
zusammenfaßt und einen HTML-Link darumherum baut, der die
CGI-Parameter edit
auf 1
und id
auf die in der Datenbank
gefundene ID des Eintrags setzt, sodaß das Skript bei einem Klick
auf den Eintrag sofort den Eintrag in der Datenbank referenzieren kann.
Der Block ab Zeile 86 kommt nur bei der Installation des Skripts
kurz zum Einsatz und ruft die Initialisierungsfunktion init_db
auf, die ab Zeile 190 definiert ist, und das Unterverzeichnis der
Pseudo-Datenbank erzeugt. Weiter setzt sie einen SQL-Create-Befehl
ab, der die Pseudo-Tabelle anlegt.
Ist überhaupt kein CGI-Parameter gesetzt (wie beim ersten Aufruf des Skripts), kommt Zeile 90 zum Einsatz und zeichnet lediglich den Seitenkopf mit dem Suchfeld und dem Reiteralphabet.
Abb.1: Eingangsformular des Adreßbuches |
Abb.2: Ein neuer Eintrag wandert ins Notizbuch |
Abb.3: Auflistung der Suchergebnisse zum Buchstaben 'H' |
001 #!/usr/bin/perl -w 002 ################################################## 003 # CGI Address Book 004 # 1999, mschilli@perlmeister.com 005 ################################################## 006 007 use CGI qw/:standard :html3/; 008 use CGI::Carp qw/fatalsToBrowser/; 009 use DBI; 010 011 my $DB_DIR = "./addressbook"; 012 my $DB_DSN = "DBI:CSV:f_dir=$DB_DIR"; 013 my $DB_USER = ""; 014 my $DB_PASSWD = ""; 015 016 my @dbfields = qw/fname lname phone email addr 017 notes/; 018 my $dbflist = join(', ', @dbfields); 019 020 my $dbh = DBI->connect($DB_DSN, $DB_USER, 021 $DB_PASSWD) or die "Cannot connect to DB"; 022 023 print header(), start_html(-BGCOLOR => 'white'), 024 h1("Adreßbuch"); 025 026 if(param('insert')) { 027 # Insert/Update record according to form fields 028 insert_rec($dbh, map { $dbh->quote(param($_)) } @dbfields); 029 page_header(); 030 031 } elsif(param('delete')) { 032 # Delete a record according to ID field 033 my $id = $dbh->quote(param('id')); 034 $dbh->do(<<EOT) or die "Cannot delete data"; 035 DELETE FROM addressbook 036 WHERE id = $id 037 EOT 038 page_header(); 039 040 } elsif(param('edit')) { 041 # Display fields for inserting/updating a rec 042 if(param('id')) { 043 $id = $dbh->quote(param('id')); 044 # ID exists - Get record and preset fields 045 my $sql = qq[SELECT id, $dbflist 046 FROM addressbook 047 WHERE id = $id]; 048 my $cursor = $dbh->prepare($sql) or 049 die "Cannot select ($sql)"; 050 $cursor->execute() or die "SQL failed"; 051 052 while(defined($row = $cursor->fetch)) { 053 my $i = 1; 054 foreach $field (@dbfields) { 055 param($field, $row->[$i++]); 056 } 057 } 058 } 059 060 print start_form(), 061 hidden(-name => 'id'), 062 table({"border" => 1}, 063 TR(td("Vorname:"), 064 td(textfield(-name => 'fname'))), 065 TR(td("Nachname:"), 066 td(textfield(-name => 'lname'))), 067 TR(td("Telefon:"), 068 td(textfield(-name => 'phone'))), 069 TR(td("Email:"), 070 td(textfield(-name => 'email'))), 071 TR(td("Adresse:"), 072 td(textarea(-name => 'addr', -rows => 3))), 073 TR(td("Notizen:"), 074 td(textarea(-name => 'notes', -rows => 3))), 075 ); 076 077 print submit(-name => 'insert', 078 -value => 'Speichern'), 079 submit(-name => 'delete', 080 -value => 'Eintrag löschen'), 081 end_form(); 082 083 } elsif(defined param('search')) { 084 page_header(); 085 keyword_search($dbh, param('search')); 086 087 } elsif(param('init')) { 088 page_header(); 089 init_db($dbh); 090 091 } else { 092 page_header(); 093 } 094 095 print end_html(); 096 097 $dbh->disconnect(); # Datenbankverbindung lösen. 098 099 ################################################## 100 sub page_header { 101 ################################################## 102 print start_form(); 103 foreach $letter ('A'..'Z') { 104 print a({href => url() . 105 "?search=$letter"}, "$letter "); 106 } 107 print a({href => url() . "?search="}, 108 " Alle Einträge"), 109 p("Suchbegriff:", 110 textfield(-name => 'search'), 111 submit(-name => 'Search', 112 -value => 'Suche starten'), 113 submit(-name => 'edit', 114 -value => 'Neuer Eintrag')); 115 print end_form(); 116 } 117 118 ################################################## 119 sub keyword_search { 120 ################################################## 121 my ($dbh, $keyword) = @_; 122 my $cursor; 123 my $where_clause = ""; 124 125 if($keyword ne "") { 126 $keyword = $dbh->quote("$keyword%"); 127 $where_clause = qq[ 128 WHERE fname CLIKE $keyword OR 129 lname CLIKE $keyword]; 130 } 131 132 my $sql = qq[ SELECT id, $dbflist 133 FROM addressbook 134 $where_clause 135 ORDER BY lname]; 136 137 $cursor = $dbh->prepare($sql) or 138 die "Select failed: $sql"; 139 140 $cursor->execute() or 141 die "Can't execute ($sql): ", $cursor->errstr; 142 143 print "<TABLE BORDER=1>\n"; 144 print TR(map { th($_) } 145 qw/Name Telefon Email Adresse Notizen/); 146 147 while(defined(my $row = $cursor->fetch)) { 148 print TR(td( 149 a({href => url() . "?id=$row->[0]&edit=1"}, 150 "$row->[2], $row->[1]"), 151 td("$row->[3]"), 152 td("$row->[4]"), td("$row->[5]"), 153 td("$row->[6]"), 154 )), "\n"; 155 } 156 print "</TABLE>\n"; 157 } 158 159 ################################################## 160 sub insert_rec { 161 ################################################## 162 my($dbh, $fname, $lname, $phone, 163 $email, $addr, $notes) = @_; 164 165 if(param('id')) { 166 # ID there, it's an update! 167 my $id = $dbh->quote(param('id')); 168 169 my $sql = qq[ 170 UPDATE addressbook 171 SET id=$id, fname=$fname, 172 lname=$lname, phone=$phone, 173 email=$email, notes=$notes 174 WHERE id = $id]; 175 176 $dbh->do($sql) or die "Update failed ($sql)"; 177 178 } else { 179 # ID not there, it's a new record! 180 my $id = time . $$; # Generate ID 181 $id = $dbh->quote($id); 182 183 my $sql = qq[ 184 INSERT INTO addressbook 185 (id, $dbflist) 186 VALUES ($id, $fname, $lname, $phone, 187 $email, $addr, $notes)]; 188 $dbh->do($sql) or die "Insert failed ($sql)"; 189 } 190 } 191 192 ################################################## 193 sub init_db { 194 ################################################## 195 my $dbh = shift; 196 197 if(! -d $DB_DIR) { 198 mkdir($DB_DIR, 0755) || 199 die "Cannot create dir $DB_DIR"; 200 } 201 202 $dbh->do(<<'EOT') or die "Cannot create table"; 203 CREATE TABLE addressbook ( 204 id char(20), 205 fname char(40), lname char(40), 206 phone char(20), email char(40), 207 addr char(100), notes char(100) 208 ) 209 EOT 210 }
Um die Komma-separierten Einträge in der Datenbank-Datei lesen zu
können, braucht das DBD::File
-Modul, das die Flatfile-Datenbank
realisiert, zunächst das Modul Text::CSV_XS
. Mit SQL::Statement
kommt dann ein kleines SQL-Maschinchen hinzu und mit diesen beiden
arbeitet dann DBD::FILE
. Das DBI
-Modul und das außerdem
verwendete Modul CGI
liegen standardmäßig Perl 5.005 bei.
Alle Module stehen auf dem CPAN zur Verfügung,
mit
perl -MCPAN -eshell > install Text::CSV_XS > install SQL::Statement > install DBD::File
kriegt Perl 5.005 den letzten Schliff und es kann losgehen.
Anschließend muß das vorgestellte Skript addr.pl
ausführbar ins
cgi-bin
-Verzeichnis des Webservers gestellt und
http://localhost/cgi-bin/addr.pl?init=1
aufgerufen werden, das erzeugt ein Unterverzeichnis und
die Datenbankdatei. Klappt das nicht, muß entweder das
Unterverzeichnis geändert (Zeile 11) oder manuell
angelegt und für den Benutzer, unter dem der Webserver
läuft (meist nobody
) ausführbar gemacht werden.
Danach sollte ein
http://localhost/cgi-bin/addr.pl
die Eingangsseite hervorzaubern, ein Klick auf den ``Insert new Record''-Button zeigt das Formular an, das zur Eingabe des ersten Eintrags einlädt.
Da das Format der Datenbankdatei lesbar ist, können natürlich auch alte Datenbestände einfach importiert werden. Eine Einschränkung muß jedoch erwähnt werden: Noch sind keine SQL-Joins möglich, aber das sollte in naher Zukunft auch möglich sein.
Gerade habe ich im Duden nachgelesen, daß man Adreßbuch nach den neuen Rechtschreibregeln Adressbuch schreibt. Was ein Elend, ich hoffe es gildet trotzdem!
Michael Schilliarbeitet als Software-Engineer bei Yahoo! in Sunnyvale, Kalifornien. Er hat "Goto Perl 5" (deutsch) und "Perl Power" (englisch) für Addison-Wesley geschrieben und ist unter mschilli@perlmeister.com zu erreichen. Seine Homepage: http://perlmeister.com. |