Die Datenbank läuft auf dem Server – dieses bisher gültige Dogma durchbricht die Javascript-Datenbank-Engine Public SQL. Für die rein Client-seitige Webseitenprogrammierung eröffnen sich damit interessante Möglichkeiten: Daten lassen sich ohne Nachladen visualisieren.
Praktisch jede moderne Webpräsenz basiert auf Server-seitiger Programmlogik inklusive Datenbank. Doch HTML-Seiten auf CD-ROMs müssen darauf verzichten. Fehlende Rechenkapazität auf dem Server ist ein weiteres Argument, um Programmlogik auf die Client-Seite zu verlegen.
Zwar lassen sich dynamische Elemente in Javascript leicht programmieren, aber wer mit größeren Datenmengen jongliert, vermisst schmerzlich die Datenbank. Hier springt die in Javascript programmierte Datenbank Public SQL [1] in die Bresche. Zwar kann sie aufgrund des Sicherheitsmodells von Javascript keine neuen Daten speichern, denn der Browser darf nicht per Javascript auf die Festplatte des Rechners schreiben. Doch vorgegebene Daten erschließt sie mit jedem Webentwickler geläufigen SQL-Befehlen (siehe Kasten “Installation und Anwendung”).
|
Installation und |
|---|
|
Die Installation von Public SQL, das sein Autor Jörg Siebrands unter die MIT-License gestellt hat, ist ganz einfach: Der Download der einzelnen Datei »publicsql.js« von der Downloadseite reicht aus. Von der Version 1.1 gibt es auch ein Zip-Archiv mit Dokumentation, Beispielen und einer unkomprimierten Javascript-Datei der Bibliothek mit Kommentaren. Wer Public SQL in seine Website einbinden möchte, fügt in den Kopf seiner HTML-Seite die Zeile
<script type="text/javascript"
src="publicsql.js"></script>
ein und legt die heruntergeladene Javascript-Datei im selben Verzeichnis ab. Alternativ lässt sich auch direkt »http://www.publicsql.org/publicsql.js« als Quelle angeben. Dann benutzt die Anwendung immer die neuste Version, riskiert jedoch, dass sich der Pfad irgendwann einmal ändert oder zum Zeitpunkt des Einsatzes gerade keine Netzverbindung besteht. Abfragen durch Javascript-Event auslösenUm Abfragen zu erledigen, fügen Entwickler im Body der Webseite beispielsweise eine Funktion »abfrage()« in einen »<script>«-Container vom Typ Javascript ein, die sie durch ein Ereignis auslösen. In Frage kommen etwa »<body onload=”abfrage()”>« beim Laden des Dokuments oder »<button onclick=”abfrage()”>Drück mich</button>« durch Knopfdruck. Der zentrale Aufruf, um eine SQL-Abfrage innerhalb von »abfrage()« in Auftrag zu geben, lautet:
publicSQL.query("select * from projekte",
"publicSQL.show");
Dabei erlaubt die Bibliothek einen vereinfachten SQL-Dialekt für die Abfrage (siehe Tabelle 1). Die einzelnen Relationen, hier »projekte«, erwartet sie pro Tabelle in separaten Dateien mit der Endung ».ptf«, die im gleichen Verzeichnis wie die HTML-Seite und Javascript-Bibliothek liegen. Jede Datei folgt diesem Schema: /* Portable Table Format 0.9 */ porTables[porTables.length] = new Array( "land", "summe", "aktivität", "Deutschland", 100000, "Tunnelbau", "Belgien", 23456, "Förderprojekt", [...] "Frankreich", 76543, "Infrastruktur", 3); Die ersten beiden Zeilen schreiben das Format vor, die dritte enthält Überschriften der Attribute. Alle weiteren Zeilen enthalten die Datentupel, getrennt durch Kommata. Als letzte Zeile folgt die Anzahl der Attribute pro Zeile. Damit lassen sich besonders Json- oder CSV-Dateien leicht in das Format überführen. Da eine PTF-Datei technisch aus gültigem Javascript besteht, müssen nicht notwendigerweise alle Werte eines Tupels in einer Zeile stehen. Allerdings erfordern einige Zusatzwerkzeuge außerhalb von Public SQL diese Bedingung. Callback-Funktion statt Rückgabewert Die Funktion »PublicSQL.query()« gibt selbst keinen Wert zurück. Stattdessen ruft sie asynchron die Funktion auf, die sie als zweiten Parameter übergeben bekommt. Die Bibliothek liefert eine Funktion »PublicSQL.show()« mit, die mit diesem Interface umgehen kann und aus dem Ergebnis eine einfach formatierte HTML-Tabelle baut. Bei komplexeren Anfragen und deren Ergebnisdarstellung schreiben Entwickler besser eine eigene Funktion, die als Parameter ein zweidimensionales Javascript-Array erwartet und es in HTML umsetzt. Weitere Funktionen und Attribute dienen dazu, die Pfade zu den Tabellen zu definieren, sie vorab zu laden, die Sortierung zu bestimmen oder zur Fehlerbehandlung. Die Webseite enthält eine vollständige Liste der Funktionen und führt die meisten in Beispielen vor [3]. |
Public SQL unterstützt »SELECT«-Statements mit »WHERE«- und »ORDER BY«-Klauseln (Tabelle 1). Außer String- und Zahlenfeldern dürfen Tabellen auch Datumspalten enthalten. Mehrere Tabellen sind per Inner Join verknüpfbar und es gibt eine »DISTINCT«-Anweisung.
|
Tabelle 1: Befehle |
|
|---|---|
|
Schlüsselwort |
Bedeutung |
|
SELECT |
Suche in der Datenbank. Genaugenommen der einzige SQL-Befehl, |
|
DISTINCT |
Filtert mehrfache Vorkommen in der Ergebnisliste aus. |
|
FROM |
Es sind Joins von mehreren Tabellen möglich. Für jede |
|
AS |
Vergibt neue Namen für das ausgewählt Attribut. |
|
WHERE |
Die Suchbedingung kennt die Junktoren AND, OR und NOT, Klammern |
|
ORDER BY |
Sortiert nach einem oder mehreren der Ergebnisattribute. |
|
ASC |
Sortiert aufsteigend. |
|
DESC |
Sortiert absteigend. |
Der Leistungsumfang ist überschaubar, dennoch schrumpfen viele in reinem Javascript schwer lösbare Aufgaben mit Public SQL auf wenige Zeilen Code zusammen. Eine Beispielanwendung, die in beliebigen Feldern einer Datenbank mit Obst- und Gemüsesorten sucht (siehe Abbildung 1), demonstriert dies. Abbildung 2 zeigt schematisch ihren Aufbau. Die ganze Anwendung lässt sich vom FTP-Server des Linux-Magazins herunterladen [2] und natürlich auch offline ausprobieren.

Abbildung 2: Alle Macht den Eventhandlern: Erst nach einem Klick erscheinen das Dropdown »Feldauswahl« und das Eingabefeld für den Suchtext in der Seite.
Pro Tabelle benötigt Public SQL eine für Javascript erreichbare Datei: Die liegt entweder – etwa bei einer Daten-CD – im lokalen Dateisystem oder lässt sich per HTTP-Protokoll nachladen. Jede besteht aus einem Array, das sich in der Praxis leicht aus CSV-Dateien erzeugen lässt. Lediglich zwei Zeilen am Beginn und eine am Ende sind hinzuzufügen. Näheres erläutert die Dokumentation [3].
Einbahnstraße
Gibt es keinen Server, der auf per »POST« oder »GET« zugeschickte Werte mit einer neuen Seite antwortet, übernimmt eine durchgehend geöffnete Seite neben der Funktion als Suchformular auch die Ergebnisanzeige. Das knappe Gerüst für beides findet sich im HTML-Code aus Listing 1. Die Zeilen 7 bis 11 definieren ein Formular, das nur zwei Buttons enthält: Der Button »+ Suchkriterium« (Zeile 8) fügt über den verknüpften Eventhandler »add_searchfield()« eine Zeile mit einer Suchbedingung ein (Abbildung 1). Per Klick auf »Suche ausführen« (Zeile 10) geht\’s los. Unter dem Formular steht im Code ein leeres »div«-Tag (Zeile 13), in das die Funktion »execute_search()« das Suchergebnis schreibt.
|
Listing 1: |
|---|
01 <head>
02 <meta http-equiv="Content-Type"
content="text/html; charset=UTF-8">
03 <script type="text/javascript"
src="publicsql.js"></script>
04 </head>
05 <body>
06 Suche nach:
07 <form name="form" action="">
08 <button onclick="add_searchfield();
return false">
+ Suchkriterium</button>
09 <div id="inputarea"></div>
10 <button onclick="execute_search();
return false">
Suche ausführen</button>
11 </form>
12 Suchergebnis:
13 <div id="result"></div>
14 </body>
|
Um eine Suche zu initiieren, klickt der Anwender zunächst auf »+ Suchkriterium«. Damit fügt er ein Listenfeld ein, indem er das Feld wählt, in dem er suchen möchte (Abbildung 1, linke Spalte). Fürs Einfügen zuständig ist der Eventhandler »add_searchfield()« (Listing 2). Die erste Zeile der Funktion füllt »code« daher mit dem öffnenden Tag eines Dropdown-Listenfelds: »<select name=”searchindex” onclick=”add_input(Index, this)”>«. Den dort enthaltenen Onklick-Eventhandler erläutert der nächste Abschnitt. Eine Schleife (Zeilen 10 bis 14) fügt für jedes Element des Array »fieldnames« ein »option«-Tag hinzu:
<option value="0">[bitte wählen]</option> <option value="1">Obst oder Gemüse</option>
und so weiter. Zeile 15 schließt das »select«-Tag und fügt außerdem noch eine leeres »span«-Tag mit der ID »input_search Zeilenindex« hinzu. Dies erleichtert »add_input()« später das Einfügen des Eingabefelds in der zweiten Spalte. Der Zeilenzähler »fieldcount« ermöglicht es, die Divs auseinanderzuhalten.
|
Listing 2: Dynamisches |
|---|
01 var fieldcount=0;
02 var fieldnames=[ "[bitte wählen]",
03 "Obst oder Gemüse", "Farbe",
04 "Geschmack", "Größe" ];
05
06 function add_searchfield() {
07 var code = 'n<select name="search' +
08 fieldcount + '" onclick="add_input(' +
09 fieldcount + ', this)">' + 'n';
10 for (i in fieldnames) {
11 code += '<option value="' + i +
12 '">' + fieldnames[i] +
13 '</option>n';
14 }
15 code += '</select>n<span id="input_search' +
16 fieldcount + '"></span>';
17 fieldcount++;
18 var newSpan=document.createElement('div');
19 newSpan.innerHTML=code;
20 document.getElementById("inputarea").
21 appendChild(newSpan);
22 }
|
Zweite Wahl
Der Benutzer wählt im Dropdown-Feld aus einer im Code definierten Liste von Suchfeldern (Abbildung 1). Wegen des »value«-Tag erhält das in Zeile 11 erzeugte Feld einen numerischen Wert von 0 bis 4. Ein Mausklick auf das Dropdown-Feld ruft den »onclick«-Handler mit dem Zeilenzahl-Zähler »fieldcount« sowie einer Referenz auf das ihn aufrufende Dropdown-Feld (»this«) auf – bis jetzt jedoch nur theoretisch, denn erst die Zeilen 18 bis 21 fügen den HTML-Code tatsächlich in die Seite ein.

Abbildung 1: Public SQL implementiert eine lokale Suche über beliebig viele Felder einer Datenbank, indem Javascript ein dynamisches SQL-Statement erzeugt.
Anfänglicher Leerstand
Die Funktion »createElement()« legt dazu zunächst ein leeres Div-Element an, Zeile 19 füllt es mit dem Inhalt von »code«. Anschließend fügt »appendChild()« den Inhalt dem beim Laden der Seite leeren Div »inputarea« hinzu. »appendChild()« überschreibt bestehenden Code nicht, also lassen sich beliebig viele Zeilen mit Suchparametern hinzufügen. Der enthaltene »add_input()«-Eventhandler (siehe Listing 3) fügt, wie schon erwähnt, bei der Auswahl eines Suchfelds die zweite Spalte für die Eingabe eines Wertparameters hinzu. Leider funktioniert dies nicht im Konqueror, da dessen Script-Engine die dynamisch hinzugefügten Eventhandler ignoriert.
|
Listing 3: Feld von |
|---|
01 var input_code=[
02 '',
03 '<select class="result" name="type">
04 <option value="0">Obst</option>
05 <option value="1">Gemüse</option>
06 </select><br/>',
07 '<input class="result" type="text"
08 name="color"><br/>',
09 '<input class="result" type="text"
10 name="taste"><br/>',
11 '<select class="result" name="size">
12 <option value="0">klein</option>
13 <option value="1">mittel</option>
14 <option value="2">groß</option>
15 </select><br/>' ];
16
17 function add_input(n, e) {
18 document.getElementById('input_search' + n).
19 innerHTML = "n = " + input_code[e.value];
20 }
|
Über den Zeilenzähler »n« findet »add_input()« das richtige Div-Element »input_searchn«. »e« liefert eine Referenz auf das linke Dropdown-Feld, »e.value« also den dort beim Mausklick ausgewählten numerischen Wert. Diesem entsprechend konsultiert »add_input()« die Codetabelle im Array »input_code[]«. Passend zum in der linken Spalte gewählten Suchfeld steht dort der HTML-Code für das rechte Eingabefeld-Suchfeld – entweder ein weiteres Dropdown- oder ein Texteingabefeld. Auch das »name«-Attribut des Eingabefelds passt dabei zum links gewählten Feldnamen. Das dynamisch zusammenklickbare Suchformular ist nun fertig – Zeit für die Suchfunktion auf der Basis von Public SQL.
Anders als bei zum Server geschickten Formularen, wo sich die Daten automatisch in den »POST«-Headern oder »GET«-Parametern wiederfinden, muss sie die Programmlogik bei einem Client-seitig durchgehend offenem Formular erst aus dem DOM-Tree auslesen.
Beerenauslese
Das Attribut »document.Formularname.elements« wie in Zeile 7 aus Listing 4 gibt die Liste der Eingabeelemente im Formular zurück – leider inklusive Buttons, die gar keine Daten enthalten. Dazu kommt, dass nur die Eingabefelder der rechten Spalte relevant sind: Mit dem Wertepaar aus »name« und »value« enthalten sie alles, was die Datenbankabfrage benötigt. Daher hat der HTML-Code im Array »input_code« in Listing 3 vorgesorgt und mit dem Klassenattribut »result« ein Filterkriterium bereitgestellt.
|
Listing 4: SQL-Abfrage |
|---|
01 function execute_search(){
02 var element;
03 var sql;
04 var firstloop = true;
05
06 sql = "SELECT * FROM plants WHERE ";
07 for (e in document.form.elements) {
08 element = document.form.elements[e];
09 if (element) {
10 if (element.className == 'result') {
11 if (!firstloop) sql += " AND ";
12 firstloop = false;
13 sql += element.name + " = '" +
14 element.value + "'";
15 }
16 }
17 }
18 sql += " ORDER BY type, size, name";
19 publicSQL.query(sql, "show_result");
20 }
|
Nun fällt es der For-Schleife in den Zeilen 7 bis 16 in der Funktion »execute_search« leicht, die gewünschten Daten über das Attribut »element.className« auszusieben. Im Internet Explorer enthält die Liste der Formularelemente »document.form.elements« leere Einträge – warum weiß wohl nur Microsoft. Dort ist es also nötig, vorher abzufragen, ob »elements[e]« überhaupt gesetzt ist (Zeile 9). Aus »element.name« und »element.value« aller Elemente der Klasse »result« baut die Schleife von Zeile 7 bis 16 die »WHERE«-Klausel für das SQL-Statement zusammen. Bei einer bloß lesbaren Datenbank, deren Daten zudem Client-seitig vorliegen, stellt SQL-Injection zum Glück keine Bedrohung dar.
Skript baut Abfrage dynamisch zusammen
Zeile 11 verknüpft die Bedingungen mit »AND«. Zeile 6 hat bereits den Kopf der Abfrage in die Variable »sql« geschrieben, Zeile 18 fügt noch eine »ORDER BY«-Klausel an. Die eigentliche Arbeit erledigt Zeile 19: Sie ruft Public SQL mit der erzeugten Anfrage als ersten Parameter auf. Als zweiten Parameter erwartet die Datenbank-Engine den Namen einer Callback-Funktion (»show_result()«, siehe Listing 5), die sie nach einer erfolgreichen Abfrage mit einem Ergebnis-Array als Parameter aufruft.
|
Listing 5: Ergebnis |
|---|
01 var types=["Obst", "Gemüse"];
02 var sizes=["klein", "mittel", "groß",]
03
04 function show_result(result){
05 var html = "n<table>n<tr><th>Name</th>
06 <th>Obst oder Gemüse</th>
07 <th>Farbe</th>
08 <th>Geschmack</th>
09 <th>Größe</th></th>";
10 for (line =1; line < result.length; line++) {
11 html+="n<tr>";
12 html+="<td>"+result[line][1]+"</td>";
13 html+="<td>"+types[result[line][2]]+"</td>";
14 html+="<td>"+result[line][3]+"</td>";
15 html+="<td>"+result[line][4]+"</td>";
16 html+="<td>"+sizes[result[line][5]]+"</td>";
17 html+="</tr>";
18 }
19 html+="</table>";
20 document.getElementById('result').
21 innerHTML = html;
22 }
|
Das von Public SQL gelieferte Ergebnis hat folgenden Aufbau: »result[0][Spalte 0 bis Spalte n]« enthält die Feldnamen, »result[n][Spalte 0 bis Spalte x]« die Daten der n-ten Zeile. »<show_result()« braucht also nur noch über die Array-Elemente ab Index 1 zu iterieren und die Spalten auf die Tabellenzellen zu verteilen. In den Spalten zwei und fünf steht nicht der numerische Wert direkt aus der Datenbank, sondern die entsprechende Langtextform aus den Arrays »types« und »sizes« (Listing 5, Zeilen 1 und 2). Ab Zeile 20 schreibt Javascript den entstanden HTML-Code an die im statischen Grundgerüst vorgesehene Stelle, das »div«-Tag in Listing 1, Zeile 9.
Nützlich mit Einschränkung
Die Beispielanwendung lässt ahnen, welche wertvollen Dienste Public SQL bei der Javascript-Programmierung leisten kann: Für eine aufwändige Suchfunktion muss der Entwickler bloß den passenden SQL-Code verfassen statt aufwändige Abfragen in Javascript zu formulieren. Das Gleiche gilt für interaktive Anwendungen, die zur Laufzeit Daten auswählen, rekombinieren und visualisieren.
Unpraktisch ist allerdings, dass die Bibliothek die Daten an eine Callback-Funktion weiterreicht statt sie als Array oder Objekt zurückzugeben. Nur innerhalb der Callback-Funkion steht das Suchergebnis bereit. Wer versucht das Ergebnisarray an eine globale Variable zu binden, damit es im weiteren Programmablauf zur Verfügung steht, scheitert in der Regel an einer Race-Condition.
|
Performance |
|---|
|
Zwangsläufig kann Public SQL es bei der Abfragegeschwindigkeit nicht mit klassischen Datenbanksystemen aufnehmen. In der Praxis lohnt jedoch ein Blick auf die Gesamtrechnung: Für das Benutzererlebnis ist die Zeit zwischen Klick und der Reaktion im Browser entscheidend. Dafür nehmen Anwender gerne einen einmaligen Ladevorgang von wenigen Sekunden in Kauf. Datenmengen schnell herunterladenIn 1 bis 2 Sekunden lässt sich über einen durchschnittlichen DSL-Anschluss gut 1 MByte an Daten herunterladen, das sind oft schon einige 10 000 Datensätze. Wenn nun die Latenz der Netzverbindung (besonders im mobilen Internet), des Webservers, seiner Anwendung und der dahinterstehenden Datenbank größer ist als die lokal ausgeführter Anwendungen in Javascript, reagieren sie für den Anwender fließender als die Online-Variante. Die jeweiligen Grenzen unterliegen jedoch vielen Parametern und Konfigurationen. In einem typischen Testaufbau durchsuchte Public SQL eine exemplarisch ausgewählte vierspaltige Tabelle mit gut 15 000 EU-Förderprojekten in etwa 20 Sekunden. Entwickler sollten berücksichtigen, dass der dynamische Aufbau der HTML-Tabelle selbst einen Großteil der Zeit in Anspruch nimmt. Unempfindlich für komplexe SuchanfragenJoins oder komplexere Anfragen hatten in Tests kaum Auswirkungen auf die Antwortzeiten. Dennoch müssen sich Entwickler darauf einstellen, dass bei den beschriebenen Größenordnungen der Browser nachfragt, ob Javascript so viel Rechenzeit verbrauchen soll – bei PTF-Dateien mit vielen Datenelementen weigerte sich Firefox sogar neue Objekte anzulegen. |
Die Zahl der möglichen Datensätze ist begrenzt: Bei Tabellen über 10 000 Einträgen liefert die Mozilla-Engine den Fehler »Too many constructor Elements« (siehe Kasten “Performance”), der Internet Explorer schafft etwas mehr. Bei Abfragen aus Tabellen dieser Größe mit einer Ergebnismenge von nicht mehr als rund 100 Datensätzen reagiert der Browser noch ohne spürbare Verzögerung.
|
Infos |
|---|
|
[1] Public SQL: [http://www.publicsql.org] [2] Beispielanwendung des Artikels:[ftp://ftp.linux-magazin/pub/listings/magazin/2010/08/publicsql] [3] Dokumentation: [http://www.publicsql.org/dokumentation.htm] |
|
Der Autor |
|---|
|
Peter Kreußel hat zahlreiche Artikel für das Linux-Magazin verfasst. Er arbeitet als freier Autor und Webenwickler. Sein besonderes Interesse gilt semantischen Technologien und Cross-Media-Publishing. |






