Groovy: Datenbanken
Kaum eine größere Anwendung kommt ohne eine Datenbank aus, und in aller Regel handelt es sich dabei um ein relationales Datenbank-Management-System (RDBMS), auf die über die standardisierte Abfragesprache SQL zugegriffen wird. Dementsprechend gut ausgebaut ist inzwischen die Unterstützung in Java für die Arbeit mit solchen Datenbanken; für praktisch alle relevanten Produkte gibt es Treiber, die über Java-Schnittstellen verfügen oder ganz in Java geschrieben sind. Für die Treiber gibt es einen Satz fest definierter Interfaces, die als JDBC (Java Database Connectivity) bezeichnet werden. Die JDBC-Schnittstellen können natürlich auch aus Groovy genau so wie in Java verwendet werden. In den Groovy-Bibliotheken finden sich aber einige hilfreiche Klassen, in denen die Möglichkeiten, die diese Sprache bietet, phantasievoll umgesetzt sind.
Anhand einiger sehr einfacher Beispiele wollen wir Ihnen zeigen, welche prinzipiellen Ansätze dabei verfolgt werden. Die Umsetzung auf komplexere, realitätsnähere Situationen dürfte Ihnen aber nicht schwer fallen, sofern Sie sich mit Datenbankzugriffen unter Java generell etwas auskennen.
Um mit einer Datenbank arbeiten zu können, müssen Sie natürlich eine solche zur Verfügung haben. Die folgenden sehr einfachen Beispiele sind etwas auf das gerade für Experimentierzwecke ausgesprochen populäre Datenbanksystem HSQLDB abgestimmt. Sie können natürlich auch ein anderes Datenbanksystem verwenden, müssen dann aber ein paar Anpassungen beim Herstellen der Datenbankverbindung und eventuell in den Anweisungen zum Erstellen der Tabellen vornehmen. Falls Sie es aber mit HSQLDB versuchen wollen, besorgen Sie sich einfach eine neuere Version von der Website unter http://hsqldb.org/. Packen Sie das Archiv auf Ihrem Rechner aus und kopieren Sie die das darin enthaltene Archiv hsqldb.jar in Ihre Groovy-Library-Verzeichnis .groovy/lib unterhalb Ihres Home-Verzeichnisses ab. Es wird dann beim nächsten Start von Groovy automatisch in den Klassenpfad eingebunden.
Die Klasse Sql
[Bearbeiten]Während Sie es in Java beim Arbeiten mit JDBC immer mit einer java.sql.Connection zu tun haben, die eine Datenbankverbindung repräsentiert, verwenden Sie in Groovy lieber ein Objekt vom Typ groovy.sql.SQL; es kapselt die Connection und bietet eine Reihe für den Programmierer erfreulicher zusätzlicher Möglichkeiten.
Datenbankverbindung herstellen
[Bearbeiten]Wir wollen eine Datenbank mit einigen Grunddaten zu den Staaten der Erde verwenden. Erzeugen Sie dazu ein neues SQL-Objekt unter Angabe der Datenbank-URL, des Datenbankbenutzers und -passworts sowie der Treiberklasse.
def sql = groovy.sql.Sql.newInstance( 'jdbc:hsqldb:file:db/staatendb', // Datenbank-URL 'sa', // Datenbank-Benutzer '', // Passwort 'org.hsqldb.jdbcDriver') // Treiber-Klasse
Die URL besagt, dass wir mit einer HSQLDB-Datenbank in Dateiform arbeiten wollen. Die Datenbankdateien befinden sich unterhalb des aktuellen Arbeitsverzeichnisses in einem Unterverzeichnis db und haben alle den Namen staatendb mit verschiedenen Endungen.[1] Wenn die Datenbank nicht vorhanden ist, legt HSQDB sie standardmäßig einfach an; wir brauchen also nichts weiter vorzubereiten.
Wenn wir das SQL-Objekt nicht mehr brauchen, schließen wir es wieder und beenden damit die Ressourcen fressende Datenbankverbindung.
sql.close()
Allgemeine SQL-Befehle
[Bearbeiten]Das Sql-Objekt verfügt über eine ganze Reihe von Methoden, mit denen die unterschiedlichsten Datenbankoperationen ausgeführt werden. Die grundlegendste von ihnen ist wohl die Methode execute(), mit der Sie irgendeinen SQL-Befehl ausführen können, sofern dieser nicht zum Lesen von Datenbankdaten dient.
Mit Hilfe eines solchen SQL-Befehls wollen wir erst einmal eine Datenbank anlegen. Die Anweisung dazu ist lang, daher kommt es uns zustatten, dass wir in Groovy auch mit mehrzeiligen Strings arbeiten können.
sql.execute ''' DROP TABLE staat IF EXISTS; CREATE TABLE staat ( id INT IDENTITY, name VARCHAR(80), hauptstadt VARCHAR(80), vorwahl SMALLINT, feiertag DATE) '''
Die execute()-Methode erhält hier einen einzigen String mit zwei SQL-Befehlen. Der erste löscht die Tabelle, sofern sie schon existiert; das ist sehr praktisch zum Üben, alternativ könnte man auch die Dateien manuell löschen. Der zweite Befehl legt die Tabelle neu an und definiert die Felder für einen eindeutigen Primärschlüssel, den Namen des Landes, den Namen der Hauptstadt, die Telefonvorwahlnummer und ein Datum für den Nationalfeiertag oder Unabhängigkeitstag. Der Primärschlüssel wird von der Datenbank automatisch vergeben, darum brauchen wir uns also nicht kümmern.
Prepared Statements mit GStrings
[Bearbeiten]In der gleichen Weise könnten wir die Tabelle nun auch mit Daten zu füllen. Das würde etwa so aussehen:
sql.execute '''INSERT INTO staat (name,hauptstadt,vorwahl,feiertag) VALUES ('Afghanistan','Kabul',93,null)''' sql.execute '''INSERT INTO staat (name,hauptstadt,vorwahl,feiertag) VALUES ('Ägypten','Kairo',20,'1922-02-28')'''
Das funktioniert, aber es ist umständlich und enthält sehr viel Redundanz. Außerdem muss jede Anweisung trotz ihrer Ähnlichkeit neu zerlegt werden und es kann zu Fehlern kommen, wenn einer der einzufügenden Texte ein Hochkomma enthält.
Bei der SQL-Programmierung arbeitet man daher lieber mit prepared statements, bei denen der Anweisungstext von den Daten getrennt übergeben werden und erst vom Datenbanktreiber zusammengesetzt werden. Prepared Statements sind außerdem performanter, wenn sie mehrmals verwendet werden, und sind sicherer, weil sie keinen Ansatzpunkt für SQL-Injection-Attacken bieten.
In Groovy geht das sehr elegant mit GStrings, zum Beispiel so:
def staatEinfügen(name,hauptstadt,vorwahl,feiertag) { sql.execute """INSERT INTO staat (name,hauptstadt,vorwahl,feiertag) VALUES ($name,$hauptstadt,$vorwahl,$feiertag)""" }
Hier haben wir also die SQL-Anweisung in einem GString, der die Parameter der Methode referenziert. Damit keine Missverständnisse aufkommen: Die Parameter werden nicht einfach textuell in den Anweisungsstring eingefügt, das würde auch nicht funktionieren, denn die Parameter name, hauptstadt und feiertag müssten dann in Anführungszeichen gesetzt werden. Vielmehr analysiert das SQL-Objekt den GString, erzeugt daraus ein Prepared-Statement und übergibt dies zusammen mit den Parametern an die JDBC-Verbindung. Nun wirkt das Einfügen der Daten in der Form von Methodenaufrufen schon viel aufgeräumter:
staatEinfügen('Albanien','Tirana',355,'1912-11-28') staatEinfügen('Algerien','Algier',213,'1962-03-18')
Als Variante könnten Sie die Methode auch so definieren, dass einfach nur eine Map als Parameter dient.
def staatEinfügen (Map m) { sql.execute """INSERT INTO staat (name,hauptstadt,vorwahl,feiertag) VALUES ($m.name,$m.hauptstadt,$m.vorwahl,$m.feiertag)""" }
Dann müssen allerdings die Parameternamen beim Methodenaufruf angegeben werden. Dieses Vorgehen bietet sich insbesondere dann an, wenn es sehr viele Felder gibt, von denen nicht immer alle belegt werden.
staatEinfügen (name:'Andorra', hauptstadt:'Andorra la Vella', vorwahl:376, feiertag:'1278-09-08') staatEinfügen (name:'Angola', hauptstadt:'Luanda', vorwahl:244, feiertag:'1975-11-11')
Durch Ergebnismengen iterieren
[Bearbeiten]In den meisten Fällen sollen die in der Datenbank verstauten Informationen irgendwann auch wieder gelesen werden. Die von JDBC angebotenen Methoden dazu liefern Ergebnismengen zurück, die zwar eine Ähnlichkeit mit Listen haben, trotzdem aber in Java nur umständlich zu behandeln sind. Groovy bietet dazu einen eigenen Ansatz, bei dem die Ergebnismengen über Closures sehr einfach durchlaufen werden können und die Spaltenwerte sich wie Properties verhalten.
Sehen wir uns doch einmal an, was sich inzwischen in unserer Datenbank so angesammelt hat:
sql.eachRow('SELECT * FROM staat') { println "$it.name, $it.hauptstadt, $it.vorwahl, $it.feiertag" }
Wenn Sie schon einmal mit JDBC programmiert haben, werden Sie dies auch ausgesprochen übersichtlich finden. Wir übergeben der Methode eachRow() einfach die Select-Anweisung und einen Iterator; letzterer wird dann mit jeder Zeile des Ergebnisses aufgerufen. Das Ergebnis sieht in diesem Fall so aus:
Afghanistan, Kabul, 93, null Ägypten, Kairo, 20, 1922-02-28 Albanien, Tirana, 355, 1912-11-28 Algerien, Algier, 213, 1962-03-18 Andorra, Andorra la Vella, 376, 1278-09-08 Angola, Luanda, 244, 1975-11-11
Natürlich wollen Sie nicht immer alle Daten lesen. Wenn Auswahlkriterien angegeben werden sollen, kommt ist es auch wieder praktisch, einen GString zu benutzen. In der folgenden Anweisung wollen wir die Staaten heraussuchen, die erst im 20. Jahrhundert selbständig geworden sind.
def startDatum = new java.sql.Date(0,0,01) // = 1.1.1900 sql.eachRow("SELECT name,feiertag FROM staat WHERE feiertag>=$startDatum") { println "$it.name, $it.feiertag" }
Häufig steht von vorneherein fest, dass ein Ergebnis aus nicht mehr als einem Datensatz bestehen kann, z.B. wenn man über einen Primärschlüssel sucht. Es ist dann nicht nötig, eine Ergebnismenge zu durchlaufen. Für diesen Zweck gibt es eine Methode firstRow(), die nur die erste Zeile der Ergebnismenge liefert, diese aber als Rückgabewert.
Wie heißt noch eben die Hauptstadt von Angola?
println sql.firstRow("SELECT * FROM staat WHERE name=?",['Angola'])?.hauptstadt
Richtig. Hier müssen wir übrigens mit einem Fragezeichen, dem klassischen SQL-Platzhalter arbeiten, denn firstRow() unterstützt, wie auch einige weitere Methoden, keine GStrings. Dies ist nicht weiter tragisch, denn stattdessen können die Parameter als Liste angegeben werden. Es ist aber auch nicht schwierig, sich eine Kategorienklasse zu bauen, die eine firstRow()-Methode mit GString hinzufügt. (Mehr zu Kategorienklassen in Kapitel ##DYNAMIK##).
class SqlCategory { static Object firstRow(groovy.sql.Sql self, GString gstring) { def params = self.getParameters(gstring) def sqlcode = self.asSql(gstring, params) self.firstRow(sqlcode, params) } } // Einzeilige Abfrage mit GString-Parameter. use (SqlCategory) { land = 'Angola println sql.firstRow("SELECT * FROM staat WHERE name=$land")?.hauptstadt }
Wenn übrigens die Ergebnismenge leer ist, liefert firstRow() ein null als Ergebnis. Daher benötigen wir das Fragezeichen am Ende vor der Dereferenzierung von hauptstadt; andernfalls würden wir bei einem leeren Ergebnis eine NullPointerException erhalten.
Das SQL-Objekt bietet Ihnen noch zahlreiche weitere Methoden, die für das Arbeiten mit Datenbanken erforderlich sind. Dazu gehören commit() und rollback() für eine eigene Transaktionssteuerung. Näheres dazu im Anhang ##API##.
Datasets: Groovy statt SQL
[Bearbeiten]Neben der SQL-Klasse hat Groovy in Bezug auf Datenbanken noch etwas Anderes zu bieten, das geeignet ist, beim ersten Kontakt Überraschungen auszulösen, weil es die dynamischen Möglichkeiten von Groovy geschickt ausreizt: Das so genannte Dataset. Es repräsentiert, eine bestimmte Datenmenge in der Datenbank (Tabelle oder View) und ermöglicht einen partiellen Zugriff auf diese Daten für relativ einfache Fälle ohne jeden SQL-Code und ohne Unterstützung durch ein objektrelationalen Mapping-Tool.
Sie erhalten ein Dataset, indem Sie es unter der Angabe eines Klassennamens und eines Tabellenamens bei Ihrem SQL-Objekt anfordern:
def staaten = sql.dataSet('Staat')
Als Ergebnis haben Sie ein Objekt der Klasse groovy.sql.DataSet, das Ihnen eine Sicht auf die Datenbanktabelle wie auf eine Liste von Zeilen verschafft. Dataset ist von der Klasse SQL abgeleitet; es verfügt also über dessen gesamte Funktionalität, erweitert diese aber um Datenbankzugriffe ohne SQL-Code.
Um deren Inhalt auszugeben, brauchen wir tatsächlich kein SQL mehr, dazu reicht einfach ein Aufruf der Methode each().
staaten.each { println "$it.name, $it.hauptstadt, $it.vorwahl, $it.feiertag" }
Richtig beeindruckend wird es, wenn Sie die Daten in der Tabelle filtern möchten. Auch dies können Sie wie bei einer Liste mit einem Aufruf von findAll() erledigen. Die folgende Anweisung soll alle Staaten liefern, die in der ersten Hälfte des 20. Jahrhunderts unabhängig geworden sind.
def teilmenge = staaten.findAll { it.feiertag>='1900-01-01' && it.feiertag<'1950.01-01' } teilmenge.each { println "$it.name, $it.feiertag" }
In teilmenge haben Sie also wiederum ein DataSet-Objekt, dessen Inhalt Sie leicht auflisten können. Das Ergebnis sieht erwartungsgemäß so aus:
Ägypten, 1922-02-28 Albanien, 1912-11-28
Wenn Sie jetzt die naheliegende Vermutung haben, dass dieses Dataset nichts weiter macht, als alle Daten einzulesen und mit der Closure zu filtern, und meinen, dass Sie das auch gekonnt hätten, liegen sie allerdings etwas falsch. Tatsächlich ist es so, dass die Closure nie ausgeführt wird, sondern dass deren zur Laufzeit verfügbare Syntaxstruktur dazu verwendet wird, daraus eine SQL-Anweisung zu bilden. Und diese wird beim each()-Aufruf dann angewendet. Als Beleg können Sie sich die SQL-Anweisung sowie die anstelle der Fragezeichen einzufüllenden SQL-Parameter einfach mal ansehen:
println teilmenge.sql println teilmenge.parameters
In diesem Fall sehen SQL-Code und Parameter so aus:
select * from Staat where feiertag >= ? and feiertag < ? [1900-01-01,1950-01-01]
Natürlich steht Ihnen innerhalb der Closure nicht die ganze Breite der sprachlichen Möglichkeiten von Groovy zur Verfügung. Damit sich der Groovy-Code überhaupt in SQL-Code übersetzen lässt, ist darin nur ein Ausdruck zulässig, der aus einfachen Vergleichen und logischen Verknüpfungen besteht.
Das Dataset ermöglicht Ihnen auch, Veränderungen an der unterliegenden Tabelle vorzunehmen; derzeit können aber nur Datensätze hinzugefügt werden.
staaten.add (name:'Antigua und Barbuda', hauptstadt:"Saint John's", vorwahl:1268, feiertag:'1981-11-01')
Das ähnelt dem Aufruf unserer weiter oben selbstgebauten Methode zum Hinzufügen von Datensätzen. Allerdings brauchen wir diese Methode gar nicht zu programmieren, denn das SQL-Statement zum Einfügen von Datensätzen wird im Dataset einfach automatisch aus den in der Form von benannten Parametern übergebenen Feldnamen und Feldinhalten generiert. Eine einfache Abfrage mittels sql.eachRow(), wie oben gezeigt, beweist Ihnen, dass der zusätzliche Satz auch tatsächlich sofort in die Datenbank geschrieben worden ist.
Leider sind im Dataset die übrigen Datenbank-Operationen wie update() zum Zurückschreiben geänderter Datensätze und delete() zum Löschen von Datensätzen noch nicht implementiert, obwohl dies eigentlich nicht schwieriger sein sollte als das add(). Auch macht es im Vergleich zum generellen Zustand von Groovy einen noch etwas labilen Eindruck. Allerdings ist es ein beeindruckendes Beispiel dafür, was mit einer dynamischen Programmiersprache wie Groovy möglich ist; und innerhalb der nächsten Updates dürfte auch mit einer Vervollständigung und Perfektionierung dieser Klasse zu rechnen sein.
- ↑ Anstelle der Factory-Methode newInstance() können Sie auch einen normalen Konstruktor verwenden; in diesem Fall müssen Sie aber eine schon geöffnete Datenbankverbindung oder eine JDBC-Datasource angeben.