Aus Linux-Magazin 05/2003

Digitale Bilder mit Perl archivieren und verwalten - Teil 2

Der zweite und abschließende Teil über das digitale Bildarchiv beschreibt die zum Frontend aus dem ersten Teil passende Datenbankschnittstelle. Sie benutzt die neue Perl-Wunderdroge Class::DBI.

Das Fotoarchiv-Frontend aus dem letzten Perl-Snapshot[1] archiviert digitale Fotos in speziellen Verzeichnissen auf der Festplatte und pflegt eine Datenbank mit Informationen zu den Bildern. Das Frontend benutzt dazu ein ebenfalls in Perl geschriebenes Backend, das auf die relationale Datenbank zugreift.

Für diesen Zugriff kommt das Perl-Modul Class::DBI zum Einsatz – es legt eine Objektschicht um traditionelle relationale Datenbanken. Der Programmierer navigiert damit auf Applikationsebene unbeschwert zwischen Objekten, während unten im Motorraum die SQL-Befehle hin und her flitzen. Dabei gibt sich Class::DBI offen für neue Ideen. Für den Fall, dass optimiertes SQL doch effizienter läuft als das vorgegebene Objekt-Mapping, bietet es eine Schnittstelle an, über die man problemlos SQL-Code injizieren kann.

Das Fotoarchiv-Frontend »idb« vom letzten Monat nutzt das im Folgenden vorgestellte Perl-Modul »CameraStore.pm«, um die Daten der digitalen Fotos und ihre Beschriftungen in die Datenbank zu verlagern und von dort wieder abzuholen. Es benutzt dazu die in Tabelle 1 genannten Methoden.

Tabelle 1: »CameraStore«-Methoden

Tabelle 1: »CameraStore«-Methoden

Im Motorraum

Zwischen Bildern und Tags besteht eine n:n-Beziehung: Einem Bild können mehrere Tags anhaften, ein Tag kann zu mehreren Bildern gehören. Das erfordert datenbanktechnisch einen Klimmzug. Gäbe es nur je eine Tabelle für Bilder und Tags, müsste die Datenbank bei jedem Tag die zugehörigen langen Bild-IDs speichern und zusätzlich bei jedem Bild die passenden Tag-Strings ablegen. Diese Redundanz verschwendet nicht nur Speicherplatz, sie führt auch zu Wartungsproblemen.

Die bessere Lösung benutzt eine Vermittlertabelle »tags«, die zwischen der Bildtabelle »images« und der Tag-String-Tabelle »categories« steht (Abbildung 1). Jede einzelne Reihe in »tags« gibt an, dass dem in der Spalte »image« eingetragenen Bild das in »category« referenzierte Tag zugeordnet ist.

Die Spalten »image« und »category« der Tabelle »tags« enthalten nur so genannte Fremdschlüssel mit den passenden Werten aus den ersten Spalten der Tabellen »images« und »categories«. Letztere sind jeweils als Primärschlüssel gesetzt und enthalten automatisch hochgezählte Sequenznummern. Die in der Tabelle »tags« definierten Felder führen also nur Fremdschlüssel, die in andere Tabellen verweisen. Sie enthalten – bis auf die eigentlich irrelevante Sequenznummer »id« – keine eigenen Daten.

Abbildung 1: Die Vermittlertabelle »tags« verbindet die Bildtabelle »images« und die Tag-String-Tabelle »categories«. Sie stellt damit eine n:n-Beziehung her: Jede Zeile in »tags« verbindet ein Bild mit einer Kategorie.

Abbildung 1: Die Vermittlertabelle »tags« verbindet die Bildtabelle »images« und die Tag-String-Tabelle »categories«. Sie stellt damit eine n:n-Beziehung her: Jede Zeile in »tags« verbindet ein Bild mit einer Kategorie.

Auf der Kommandobrücke

Diese Tabellenbeziehungen lassen sich mit Class::DBI einfach modellieren. Der Programmierer muss dann nicht mehr selbst die Relationen auflösen – die Objekte verhalten sich so, als ob sie statt des Fremdschlüssels direkt die referenzierten Daten enthielten. Durch Vererbung definiert das Camera-Store-Programm dazu drei eigene Klassen für die Reihen der beteiligten Tabellen. Für die Beziehungen zwischen den Klassen nutzt es die beiden Eigenschaften »has _a()« und »has_many()« der Vaterklasse »Class::DBI«.

Enthält eine Tabellenspalte keine eigenen Daten, sondern einen Fremdschlüssel, der auf Einträge in einer anderen Tabelle verweist, dann lässt sich diese Relation über eine Class::DBI-Methode fest im Objekt verankern:

KlasseTags->has_a("category",
   "KlasseCategories");

Ab diesem Zeitpunkt liefert die folgende Methode nicht mehr den Wert der Spalte »category« in » KlasseTags«, also nicht mehr den Fremdschlüssel, sondern ein Objekt der Klasse » KlasseCategories«:

KlasseTags->category();

Die Methode wählt die Zeile der Tabelle »categories«, deren Primärschlüssel zum Fremdschlüssel in der Tabelle »tags« passt. Das zurückgelieferte Objekt ist vom Typ » KlasseCategories« und enthält alle Daten der entsprechenden Zeile.

Diese Methode beschreibt die Relation in einer Richtung: von der Tabelle mit dem Fremdschlüssel zu der Tabelle, auf die sich der Fremdschlüssel bezieht. Häufig benötigt man aber die andere Richtung: Welche Einträge in einer anderen Tabelle verweisen auf eine bestimmte Zeile in der aktuellen Tabelle? Diese Beziehung lässt sich mit »has_many()« modellieren.

Beziehungskisten

In der Tabelle »tags« liegen Felder, die mit » KlasseImages« in Verbindung stehen. Das Feld »image« enthält einen Fremdschlüssel, der sich auf den Primärschlüssel in der Tabelle »images« bezieht. Davon weiß » KlasseImages« noch nichts; folgende Methode ändert das:

KlasseImages->has_many("tags",
   "KlasseTags" => "image");

Damit sorgt die Vaterklasse »Class::DBI« für einige Vereinfachungen:

  • Jedes » KlasseImages«-Objekt erhält eine
    Methode »tags()«. Diese sucht alle Zeilen in der
    Tabelle »tags«, deren
    »image«-Fremdschlüssel zum
    Primärschlüssel in »images« passt. Die
    Treffer liefert sie als Liste von
    » KlasseTags«-Objekten zurück.
  • Die neue Methode
    » KlasseImages<$>->add_to_tags()« fügt
    einen Verweis auf das aktuelle Bild als neues Element in
    »KlasseTags<$>« ein.
  • Wenn das Programm ein » KlasseImage«-Objekt
    löscht, tilgt Class::DBI automatisch auch die passenden
    Einträge aus » KlasseTags«.

Listing 1 zeigt die Implementierung der objektorientierten Datenbankschnittstelle »CameraStore.pm«. Ganz unten in Zeile 202 angefangen zeigt sich, dass die Klasse »CameraStore::IDB::Tag« von »CameraStore::IDB« erbt, die wiederum von »Class::DBI« abstammt und die Beziehungen aller verwendeten Tabellen zur Datenbank definiert.

Listing 1:
»CameraStore.pm«

001 ###########################################
002 package CameraStore;
003 ###########################################
004 # Mike Schilli, 2003 (m@perlmeister.com)
005 ###########################################
006 use warnings;
007 use strict;
008 
009 use Class::DBI;
010 use Log::Log4perl qw(:easy);
011 
012 ###########################################
013 sub new {       # Constructor
014 ###########################################
015     my($class) = @_;
016     bless {}, $class;
017 }
018 
019 ###########################################
020 sub _img {      # INTERNAL: Get image by ID
021 ###########################################
022     my($stamp) = @_;
023 
024     my($img) = CameraStore::IDB::Image->
025                  search( stamp => $stamp );
026 
027     ERROR "No such ID: $stamp" unless
028                               defined $img;
029     return $img;
030 }
031 
032 ###########################################
033 sub _cat {   # INTERNAL: Get category by ID
034 ###########################################
035     my($cname) = @_;
036 
037     my($cat) = CameraStore::IDB::Category->
038             search( name => $cname );
039 
040     ERROR "No such tag: $cname" unless
041                               defined $cat;
042     return $cat;
043 }
044 
045 ###########################################
046 sub list_tags {   # Get all tags of one img
047 ###########################################
048     my($self, $stamp) = @_;
049 
050     my @found = ();
051 
052     (my $img = _img($stamp)) or return();
053 
054     for my $tag ($img->tags) {
055         push @found, $tag->category->name;
056     }
057 
058     return @found;
059 }
060 
061 ###########################################
062 sub add_image {  # Add new image (plus tag)
063 ###########################################
064     my($self, $stamp, $path, $cname) = @_;
065 
066     if(CameraStore::IDB::Image->search(
067       stamp => $stamp ))  {
068         ERROR "ID $stamp already exists";
069         return undef;
070     }
071 
072     CameraStore::IDB::Image->create({
073                      stamp     => $stamp,
074                      path      => $path});
075 
076     return 1 unless $cname; # No tag but ok
077 
078     return $self->add_tag($cname, $stamp);
079 }
080 
081 ###########################################
082 sub delete_image {# Remove image (and tags)
083 ###########################################
084     my($self, $stamp) = @_;
085 
086     (my $img = _img($stamp)) or
087         return undef;
088 
089     $img->delete();
090 }
091 
092 ###########################################
093 sub add_tag {          # Add a new tag name
094 ###########################################
095     my($self, $cname, $stamp) = @_;
096 
097     INFO "Adding tag $cname/$stamp";
098 
099     (my $img = _img($stamp)) or
100         return undef;
101 
102         # Add category by name
103     my $cat = CameraStore::IDB::Category->
104       find_or_create({name => $cname});
105 E
106     if(CameraStore::IDB::Tag->search(
107                    image    => $img->id,
108                    category => $cat->id)) {
109         ERROR "$stamp already has $cname";
110         return undef;
111     }
112         # Add image/cat link to tags table
113     $cat->add_to_tags({image => $img->id});
114 }
115 
116 ###########################################
117 sub delete_tag {       # Take tag off image
118 ###########################################
119     my($self, $cname, $stamp) = @_;
120 
121     INFO "Strip $cname from $stamp";
122 
123     (my $img = _img($stamp)) or
124         return undef;
125 
126     (my $cat = _cat($cname)) or
127         return undef;
128 
129     my($tag)=CameraStore::IDB::Tag->search(
130                 image    => $img->id,
131                 category => $cat->id,
132               );
133 
134     unless($tag) {
135         ERROR "No $cname on $stamp";
136         return undef;
137     }
138 
139     $tag->delete();
140 }
141 
142 ###########################################
143 sub search_tag {
144 ###########################################
145     my($self, $tag, $paths, $stamp) = @_;
146 
147     my @matches = CameraStore::IDB::Image->
148                        match($tag, $stamp);
149     my $field = $paths ? "path" : "stamp";
150     return map { $_->$field } @matches;
151 }
152 
153 ###########################################
154 package CameraStore::IDB;
155 ###########################################
156 use base q(Class::DBI);
157 __PACKAGE__->set_db('Main',
158                     'dbi:mysql:idb',
159                     'root', '');
160 
161 ###########################################
162 package CameraStore::IDB::Image;
163 ###########################################
164 use base q(CameraStore::IDB);
165 
166 __PACKAGE__->table('images');
167 __PACKAGE__->columns(
168     All => qw(id stamp path));
169 __PACKAGE__->has_many('tags',
170     'CameraStore::IDB::Tag' => 'image');
171 
172 __PACKAGE__->set_sql(rawmatch => q{
173 SELECT DISTINCT images.stamp, images.path
174 FROM categories, tags, images
175 WHERE
176   categories.name LIKE ? AND
177   categories.id = tags.category AND
178   images.id = tags.image AND
179   images.stamp LIKE ?
180 });
181 
182 sub match {
183     my ($class, $rex, $stamp) = @_;
184     $stamp = "%" unless defined $stamp;
185     my $sth = $class->sql_rawmatch;
186     $sth->execute($rex, $stamp);
187     return $class->sth_to_objects($sth);
188 }
189 
190 ###########################################
191 package CameraStore::IDB::Category;
192 ###########################################
193 use base q(CameraStore::IDB);
194 
195 __PACKAGE__->table('categories');
196 __PACKAGE__->has_many('tags',
197     'CameraStore::IDB::Tag' => 'category');
198 __PACKAGE__->columns(
199     All => qw(id name));
200 
201 ###########################################
202 package CameraStore::IDB::Tag;
203 ###########################################
204 use base q(CameraStore::IDB);
205 
206 __PACKAGE__->table('tags');
207 __PACKAGE__->columns(
208     All => qw(id image category));
209 __PACKAGE__->has_a('category',
210              'CameraStore::IDB::Category');
211 __PACKAGE__->has_a('image',
212              'CameraStore::IDB::Image');
213 1;

Praktische Bildersammlung

Zeile 206 legt fest, dass die Tabelle, auf die sich die Klasse »CameraStore::IDB::Tag« bezieht, »tags« heißt. Die Zeilen 207 und 208 definieren deren Spalten mit »id«, »image« und »category«. Dabei gilt »id« implizit als Primärschlüssel, da es an erster Stelle steht. Die beiden »has_a()«-Aufrufe definieren, dass die beiden Spalten »image« und »category« jeweils nur mit Fremdschlüsseln auf die Primärschlüssel in den Tabellen »images« und »categories« verweisen (Abbildung 1).

Wie Abbildung 2 darstellt, resultiert dies in zwei zusätzlichen Methoden »image()« und »category()« (blau gezeichnet) in der Klasse »CameraStore::IDB::Tag«. Die Methoden liefern direkt die entsprechenden Objekte in »CameraStore::IDB::Image« und »CameraStore::IDB::Category«. Die rechteckigen Kästen neben den abgerundeten Klassen in Abbildung 2 enthalten Methoden, die auf die Attribute (Spaltenwerte) jedes Objekts dieser Klasse zugreifen – eine kostenlose Serviceleistung von Class::DBI.

Ab Zeile 191 definiert die Klasse »CameraStore::IDB::Category« eine Abstraktion der Tabelle »categories« (rechts unten in Abbildung 1 zu sehen). Ein Tag-String (nachfolgend auch Kategorie genannt) kann mit mehreren Einträgen in der »tags«-Tabelle verknüpft sein, wenn mehrere Bilder zur selben Kategorie gehören. Daher definiert Zeile 196 die »has_many()«-Beziehung. Sie bewirkt unter anderem, dass ein »CameraStore::IDB::Category«-Objekt eine »tags()«-Methode erhält, die alle Tag-Einträge in »CameraStore::IDB::Tag« als Liste zurückgibt. Das spiegelt die 1:n-Beziehung wider (siehe Abbildung 2).

Class::DBI sorgt automatisch dafür, dass jedes »CameraStore::IDB::Category«-Objekt eine »name()«-Methode enthält, die den Tag-String (die Kategorie) des jeweiligen Eintrags in der »categories«-Tabelle liefert. Die Methode »id()« gibt den Wert der Primärschlüsselspalte »id« an.

Die »images«-Tabelle wird von der Klasse »CameraStore::IDB::Image« – ab Zeile 162 definiert – repräsentiert. Die Tabelle besitzt die Spalten »id«, »stamp« (der Zeitstempel des Fotos) und »path« (der Pfad zur Bilddatei). Sie stellt außerdem ihre »has_many«-Beziehung zur »tags«-Tabelle zur Schau, die alle zu einem Bild gehörigen »tags«-Einträge als Liste zurückliefert.

Abbildung 2: Das Perl-Modul Class::DBI kapselt die Datenbanktabellen in Klassen. An die Stelle aufwändiger SQL-Statements treten einfache Methodenaufrufe, die auch die Relationen berücksichtigen.

Abbildung 2: Das Perl-Modul Class::DBI kapselt die Datenbanktabellen in Klassen. An die Stelle aufwändiger SQL-Statements treten einfache Methodenaufrufe, die auch die Relationen berücksichtigen.

Performance-Fallen vermeiden

Auch Class::DBI hat Grenzen – objektorientierte Hüllen um relationale Datenbanken sausen manchmal in böse Performance-Fallen. Auch konnten objektorientierte Datenbanken ihre relationalen Verwandten noch nicht ablösen – die Oracle-Türme in Redwood-City, an denen ich immer auf dem Highway 101 vorbeifahre, wurden noch nicht in Getreidesilos umgewandelt.

Schön ist an Class::DBI aber, dass es dem Entwickler durchaus die Möglichkeit einräumt, auch seinen von daheim mitgebrachten SQL-Code zu verzehren: Die ab Zeile 172 aufgerufene »set_sql()«-Methode definiert eine komplizierte Abfrage über alle drei Tabellen, die alle Bilder zurückliefert, deren Tag-String einem vorgegebenen Suchmuster entspricht. Die beiden Fragezeichen sind Platzhalter für die Abfrage. Sie fügen den Suchstring und eventuell noch die ID eines Bildes ein, falls die Abfrage nur feststellen soll, ob ein Tag eines bestimmten Bildes passt oder nicht. Durch den »DISTINCT«-Befehl landet jedes Bild maximal einmal im Ergebnis.

Diese Definition unter dem Namen »rawmatch« (Zeile 172) bringt »CameraStore::IDB::Image« dazu, eine »sql_rawmatch()«-Methode anzubieten. Die ab Zeile 182 definierte Methode »match()« nutzt »sql_rawmatch()«, um die SQL-Abfrage zu formulieren und ein Statement-Handle der darunter liegenden DBI-Schnittstelle zu erhalten. Danach schickt »execute()« die SQL-Abfrage an die Datenbank. »sth_to_objects()« wandelt das Ergebnis wieder in Objekte der Abstraktionsschicht um.

Die »match()«-Methode nimmt als erstes Argument den (eventuell als Suchmuster wie »Freu%« angegebenen) Kategorie-Suchbegriff entgegen und als optionales zweites Argument die ID eines Bildes. Fehlt das zweite Argument, wird es in Zeile 184 auf »%« gesetzt, sodass im SQL-Statement »images.stamp LIKE %« steht. Der SQL-Prozessor der Datenbank wird diese immer zutreffende Bedingung hoffentlich wegoptimieren.

Das API des Camera Store

Bevor »CameraStore.pm« öffentlich zugängliche Methoden definiert, legt es in den Zeilen 20 und 33 die beiden nur intern benutzten Funktionen »_img()« und »_cat()« an. Sie liefern zu einer vorgegebenen Image-ID (Zeitstempel) oder zu einem Tag-String (Kategorie) die passenden Objekte, indem sie die Tabellen »images« oder »categories« nach entsprechenden Einträgen durchforsten und im Erfolgsfall Referenzen auf Objekte zurückgeben, die die Reiheninformation enthalten.

Sollte jemand nicht existierende Zeitstempel oder Kategorien angeben, benutzen die Methoden das »ERROR«-Makro von »Log::Log4perl«, um eine kurze Fehlermeldung auszugeben, bevor sie ein »undef«-Ergebnis zurückliefern. Beide Funktionen werden von den öffentlichen API-Methoden genutzt (um Tipparbeit zu sparen), sie werden aber nicht nach draußen exportiert.

Die ab Zeile 46 definierte Methode »list _tags()« holt, wie gerade erläutert, mit »_img()« das zu einer vorgegebenen Image-ID gehörende Objekt und führt dessen »tags()«-Methode aus, um die Liste der passenden Tag-Objekte aus der »tags«-Tabelle zu erhalten. Mit »$tag ->category->name()« navigiert sie von jedem gefundenen Objekt blitzschnell zum entsprechenden Eintrag in der »categories«-Tabelle (»category()«-Methode) und dann zu deren »name«-Spalte (»name()«-Methode).

Die Methode »add_image()« (Zeile 62) nimmt eine Image-ID (Zeitstempel), den Pfad zur Bilddatei und eventuell einen Tag-String entgegen. Zunächst bemüht sie die »search()«-Methode, um einen bereits existierenden Eintrag zu finden, und bricht den Vorgang mit einer Fehlermeldung ab, falls sie ein Bild mit gleichem Zeitstempel in der Datenbank findet. Damit die aufrufende Funktion weiß, was Sache ist, gibt sie in diesem Fall »undef« zurück. Falls die Suche erfolglos verlief, bemüht Zeile 72 die »create()«-Methode, um ein neues Bild in die Tabelle »images« einzufügen. Falls kein Tag-String angegeben wurde, kehrt Zeile 76 vorzeitig zurück, falls doch, delegiert Zeile 78 das Einfügen des Tags an die Methode »add_tag()«.

Bilder einfügen und löschen

Das beim letzten Mal vorgestellte Frontend »idb« hat eine »CameraStore.pm«-Methode gar nicht genutzt, die für künftige Applikation aber gelegen kommen könnte: »delete_image()« (ab Zeile 82) entfernt ein Bild aus der Datenbank. Nachdem Zeile 86 das Image-Objekt geholt hat, muss Zeile 89 nur noch dessen »delete()«-Methode auslösen. Damit löscht sie nicht nur den Eintrag in der Tabelle »images«, sondern auch alle Reihen in »tags«, die sich auf das Bild beziehen. Die weiter unten definierte Beziehung »has_many()« sorgt für diese Automatik – sehr praktisch!

Die ab Zeile 93 definierte Methode »add _tag()« nutzt die »find_or_create()«-Methode des »Category«-Objekts, um die laufende Nummer einer bereits bestehenden Reihe in der »categories«-Tabelle herauszufinden. Falls noch kein Eintrag zum angegebenen Tag-String existiert, legt »find_or_create()« eine neue Kategorie an und gibt deren Nummer zurück.

Neue Tags für alte Bilder

Die danach aufgerufene »search()«-Methode des »Tag«-Objekts findet heraus, ob das Bild bereits dem hereingereichten Tag zugeordnet ist: Falls dies der Fall ist, spuckt »add_tag()« eine Fehlermeldung aus, bricht den Vorgang ab und gibt »undef« zurück. Falls nicht, hängt die »add_to_tags()«-Methode des »Category«-Objekts die Bild-Kategorie-Kombination an die »tags«-Tabelle an. »delete _tag()« ab Zeile 117 hantiert ähnlich – sie sucht erst das entsprechende Tag-Objekt und führt anschließend dessen »delete()«-Methode aus.

Die oft genutzte Suchmethode »search _tag()« (Zeile 143) gerät ziemlich kurz: Grund ist der explizit in der Klassendefinition angegebene SQL-Befehl. Sie muss in Zeile 147 lediglich die neu definierte »match()«-Methode der Klasse »CameraStore::IDB::Image« aufrufen, um eine Liste mit passenden Image-Objekten zu erhalten.

Der Eingabeparameter »$paths« gibt an, ob der Benutzer IDs oder Pfadnamen zurückhaben will. Zeile 149 setzt die Variable »$field« auf den Namen der gewünschten Spalte der »images«-Tabelle. Zeile 150 jagt alle als passend gelieferten »Image«-Objekte durch einen »map«-Transformator, der zu jedem Objekt entweder die »stamp()«- oder die »path()«-Methode aufruft und alle zusammen als Liste zurückliefert.

Installation

Die von »CameraStore.pm« benötigten Module DBI, DBD::mysql, Class::DBI und Log::Log4perl lassen sich am einfachsten mit einer CPAN-Shell installieren. Zu beachten ist aber, dass die Applikation, die das Camera-Store-Modul nutzt, »Log::Log4perl« mindestens mit der Priorität »ERROR« initialisieren sollte. Am besten ruft sie »Log::Log4perl ->init($INFO)« im »:easy«-Modus auf, wie es »idb« letztes Mal tat. So erreichen die ausgegebenen Fehlermeldungen auch den Bildschirm.

Um die Datenbank zu initialisieren, kommt auch dieses Mal ein Shell-Skript zum Einsatz. Es nutzt die Client-Programme »mysql« und »mysqladmin«, um die Datenbank und die darin liegenden Tabellen zu initialisieren. Eine eventuell schon vorhandene Datenbank gleichen Namens wird dabei gnadenlos zubetoniert, also Vorsicht!

Die besten Datenbanktricks stehen übrigens in[2]. Dieses Kochbuch für Praktiker liefert schnelle Lösungen für Probleme, bei denen sich schon Generationen von Datenbankanfängern die Haare gerauft haben. Wer das hervorragende Buch nicht kaufen will, könnte es auch auf Safari lesen – den Online-Subscription-Service von O\’Reilly kann ich hier aber nicht uneingeschränkt empfehlen, da er teuer und langsam ist.

Vielleicht sollte O\’Reilly einfach die Microsoft-Webserver rauswerfen und professionelle, skalierbare Technologie einsetzen? Can you spell A-p-a-c-h-e? Alles nur eine Frage der Zeit. Bis nächsten Monat. (fjl)

Listing 2:
»sql.sh«

01 DB=idb
02 
03 mysqladmin -f --user=root drop $DB
04 mysqladmin --user=root create $DB
05 
06 mysql --user=root --database=$DB <<EOT
07   CREATE TABLE images (
08     id    INT AUTO_INCREMENT,
09     stamp VARCHAR(32),
10     path  VARCHAR(255),
11     PRIMARY KEY (id)
12   )
13 EOT
14 
15 mysql --user=root --database=$DB <<EOT
16   CREATE TABLE categories (
17     id    INT AUTO_INCREMENT,
18     name  VARCHAR(128) UNIQUE,
19     PRIMARY KEY (id)
20   )
21 EOT
22 
23 mysql --user=root --database=$DB <<EOT
24   CREATE TABLE tags (
25     id       INT AUTO_INCREMENT,
26     image    INT,
27     category INT,
28     PRIMARY KEY (id)
29   )
30 EOT

Infos

[1] Michael Schilli, “Digitale Plattenkamera”: Linux-Magazin 04/03: [https://www.linux-magazin.de/Artikel/ausgabe/2003/04/perl/perl.html]

[2] Paul DuBois, “MySQL Cookbook”: O\’Reilly 2002, ISBN 0596001452

[3] Listings zum Artikel: [ftp://www.linux-magazin.de/pub/listings/magazin/2003/05/Perl]

Der
Autor


Michael Schilli arbeitet als Web-Engineer für AOL/Netscape in Mountain View, 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 ist [http://perlmeister.com].

LINUX-MAGAZIN KAUFEN
EINZELNE AUSGABE Print-Ausgaben Digitale Ausgaben
ABONNEMENTS Print-Abos Digitales Abo
TABLET & SMARTPHONE APPS Readly Logo
E-Mail Benachrichtigung
Benachrichtige mich zu:
0 Kommentare
Älteste
Neuste Beste Bewertung
Inline Feedbacks
Alle Kommentare anzeigen
Nach oben