JDBC mit Groovy

JDBC mit Groovy

1. Einführung

In diesem Artikel wird erläutert, wie relationale Datenbanken mitJDBC mithilfe von idiomatischem Groovy abgefragt werden.

JDBC ist zwar relativ niedrig, bildet jedoch die Grundlage für die meisten ORMs und andere High-Level-Datenzugriffsbibliotheken auf der JVM. Natürlich können wir JDBC auch direkt in Groovy verwenden. Es hat jedoch eine ziemlich umständliche API.

Zum Glück baut die Groovy-Standardbibliothek auf JDBC auf, um eine saubere, einfache und dennoch leistungsstarke Benutzeroberfläche zu bieten. Wir werden uns also mit dem Groovy SQL-Modul befassen.

Wir werden uns JDBC in Plain Groovy ansehen und dabei kein Framework wie Spring berücksichtigen, für das wirother guides haben.

2. JDBC und Groovy Setup

Wir müssen dasgroovy-sql-Modul in unsere Abhängigkeiten aufnehmen:


    org.codehaus.groovy
    groovy
    2.4.13


    org.codehaus.groovy
    groovy-sql
    2.4.13

Es ist nicht erforderlich, es explizit aufzulisten, wenn wir groovy-all verwenden:


    org.codehaus.groovy
    groovy-all
    2.4.13

Wir können die neueste Version vongroovy, groovy-sql undgroovy-all auf Maven Central finden.

3. Verbindung zur Datenbank herstellen

Das erste, was wir tun müssen, um mit der Datenbank zu arbeiten, ist eine Verbindung zu ihr herzustellen.

Lassen Sie uns diegroovy.sql.Sql-Klasse einführen, die wir für alle Operationen in der Datenbank mit dem Groovy SQL-Modul verwenden.

Eine Instanz vonSql repräsentiert eine Datenbank, mit der wir arbeiten möchten.

an instance of Sqlisn’t a single database connection. Wir werden später über Verbindungen sprechen. Machen wir uns jetzt keine Sorgen. Nehmen wir einfach an, dass alles magisch funktioniert.

3.1. Verbindungsparameter angeben

In diesem Artikel werden wir eine HSQL-Datenbank verwenden, eine leichte relationale Datenbank, die hauptsächlich in Tests verwendet wird.

Eine Datenbankverbindung benötigt eine URL, einen Treiber und Zugangsdaten:

Map dbConnParams = [
  url: 'jdbc:hsqldb:mem:testDB',
  user: 'sa',
  password: '',
  driver: 'org.hsqldb.jdbc.JDBCDriver']

Hier haben wir uns entschieden, diejenigen mitMap anzugeben, obwohl dies nicht die einzig mögliche Wahl ist.

Wir können dann eine Verbindung von der KlasseSqlerhalten:

def sql = Sql.newInstance(dbConnParams)

In den folgenden Abschnitten erfahren Sie, wie Sie es verwenden.

Wenn wir fertig sind, sollten wir immer alle zugehörigen Ressourcen freigeben:

sql.close()

3.2. Verwenden vonDataSource

Insbesondere in Programmen, die auf einem Anwendungsserver ausgeführt werden, wird häufig eine Datenquelle verwendet, um eine Verbindung zur Datenbank herzustellen.

Wenn wir Verbindungen bündeln oder JNDI verwenden möchten, ist eine Datenquelle die natürlichste Option.

DieSql-Klasse von Groovy akzeptiert Datenquellen einwandfrei:

def sql = Sql.newInstance(datasource)

3.3. Automatische Ressourcenverwaltung

Es ist mühsam, daran zu denken,close() aufzurufen, wenn wir mit einerSql-Instanz fertig sind. Maschinen erinnern sich schließlich viel besser an Dinge als wir.

MitSql können wir unseren Code in einen Abschluss einschließen und Groovyclose() automatisch aufrufen lassen, wenn die Steuerung ihn verlässt, auch in Ausnahmefällen:

Sql.withInstance(dbConnParams) {
    Sql sql -> haveFunWith(sql)
}

4. Abgeben von Anweisungen für die Datenbank

Nun können wir zu den interessanten Dingen übergehen.

Die einfachste und nicht spezialisierte Möglichkeit, eine Anweisung für die Datenbank auszugeben, ist die Methodeexecute:

sql.execute "create table PROJECT (id integer not null, name varchar(50), url varchar(100))"

Theoretisch funktioniert es sowohl für DDL / DML-Anweisungen als auch für Abfragen. Das oben stehende einfache Formular bietet jedoch keine Möglichkeit, die Abfrageergebnisse zurückzugewinnen. Wir hinterlassen Anfragen für später.

Dieexecute-Methode hat mehrere überladene Versionen, aber wir werden uns in späteren Abschnitten noch einmal mit den fortgeschritteneren Anwendungsfällen dieser und anderer Methoden befassen.

4.1. Daten einfügen

Zum Einfügen von Daten in kleinen Mengen und in einfachen Szenarien ist die zuvor beschriebene Methodeexecutevollkommen in Ordnung.

Für Fälle, in denen wir Spalten generiert haben (z. B. mit Sequenzen oder automatischem Inkrementieren) und die generierten Werte kennen möchten, gibt es eine dedizierte Methode:executeInsert.

Fürexecute betrachten wir nun die einfachste verfügbare Methodenüberladung, wobei komplexere Varianten für einen späteren Abschnitt übrig bleiben.

Angenommen, wir haben eine Tabelle mit einem automatisch inkrementierten Primärschlüssel (Identität in HSQLDB):

sql.execute "create table PROJECT (ID IDENTITY, NAME VARCHAR (50), URL VARCHAR (100))"

Fügen Sie eine Zeile in die Tabelle ein und speichern Sie das Ergebnis in einer Variablen:

def ids = sql.executeInsert """
  INSERT INTO PROJECT (NAME, URL) VALUES ('tutorials', 'github.com/eugenp/tutorials')
"""

executeInsert verhält sich genau wieexecute, aber was gibt es zurück?

Es stellt sich heraus, dass der Rückgabewert eine Matrix ist: Seine Zeilen sind die eingefügten Zeilen (denken Sie daran, dass durch eine einzelne Anweisung mehrere Zeilen eingefügt werden können) und seine Spalten sind die generierten Werte.

Es klingt kompliziert, aber in unserem Fall, der bei weitem am häufigsten vorkommt, gibt es eine einzelne Zeile und einen einzelnen generierten Wert:

assertEquals(0, ids[0][0])

Eine nachfolgende Einfügung würde einen generierten Wert von 1 zurückgeben:

ids = sql.executeInsert """
  INSERT INTO PROJECT (NAME, URL)
  VALUES ('REST with Spring', 'github.com/eugenp/REST-With-Spring')
"""

assertEquals(1, ids[0][0])

4.2. Aktualisieren und Löschen von Daten

In ähnlicher Weise gibt es eine spezielle Methode zum Ändern und Löschen von Daten:executeUpdate.

Auch dies unterscheidet sich vonexecute nur in seinem Rückgabewert, und wir werden nur die einfachste Form betrachten.

Der Rückgabewert ist in diesem Fall eine Ganzzahl, die Anzahl der betroffenen Zeilen:

def count = sql.executeUpdate("UPDATE PROJECT SET URL = 'https://' + URL")

assertEquals(2, count)

5. Abfragen der Datenbank

Wenn wir die Datenbank abfragen, wird es langsam fetzig.

Der Umgang mit der Klasse JDBCResultSetmacht nicht gerade Spaß. Zum Glück bietet Groovy darüber eine schöne Abstraktion.

5.1. Iterieren über Abfrageergebnisse

Während Loops so altmodisch sind, stehen wir heutzutage alle auf Verschlüsse.

Und Groovy ist hier, um unseren Geschmack zu treffen:

sql.eachRow("SELECT * FROM PROJECT") { GroovyResultSet rs ->
    haveFunWith(rs)
}

Die MethodeeachRowgibt unsere Abfrage für die Datenbank aus und ruft für jede Zeile einen Abschluss auf.

Wie wir sehen können,a row is represented by an instance of GroovyResultSet, eine Erweiterung von einfachen altenResultSet mit einigen zusätzlichen Extras. Lesen Sie weiter, um mehr darüber zu erfahren.

5.2. Zugriff auf Ergebnismengen

Zusätzlich zu allenResultSet-Methoden bietetGroovyResultSet einige praktische Dienstprogramme.

Hauptsächlich werden benannte Eigenschaften angezeigt, die mit den Spaltennamen übereinstimmen:

sql.eachRow("SELECT * FROM PROJECT") { rs ->
    assertNotNull(rs.name)
    assertNotNull(rs.URL)
}

Beachten Sie, dass bei Eigenschaftsnamen die Groß- und Kleinschreibung nicht berücksichtigt wird.

GroovyResultSet bietet auch Zugriff auf Spalten mithilfe eines auf Null basierenden Index:

sql.eachRow("SELECT * FROM PROJECT") { rs ->
    assertNotNull(rs[0])
    assertNotNull(rs[1])
    assertNotNull(rs[2])
}

5.3. Seitennummerierung

Wir können die Ergebnisse leicht paginieren, d. H. Nur eine Teilmenge laden, beginnend mit einem Offset bis zu einer maximalen Anzahl von Zeilen. Dies ist beispielsweise bei Webanwendungen ein häufiges Problem.

eachRow und verwandte Methoden haben Überladungen, die einen Offset und eine maximale Anzahl zurückgegebener Zeilen akzeptieren:

def offset = 1
def maxResults = 1
def rows = sql.rows('SELECT * FROM PROJECT ORDER BY NAME', offset, maxResults)

assertEquals(1, rows.size())
assertEquals('REST with Spring', rows[0].name)

Hier gibt die Methoderows eine Liste von Zeilen zurück, anstatt sie wieeachRow zu durchlaufen.

6. Parametrisierte Abfragen und Anweisungen

In den meisten Fällen werden Abfragen und Anweisungen zur Kompilierungszeit nicht vollständig behoben. Sie haben in der Regel einen statischen und einen dynamischen Teil in Form von Parametern.

Wenn Sie über die Verkettung von Zeichenfolgen nachdenken, hören Sie jetzt auf und lesen Sie mehr über SQL Injection!

Wir haben bereits erwähnt, dass die Methoden, die wir in den vorherigen Abschnitten gesehen haben, für verschiedene Szenarien viele Überladungen aufweisen.

Lassen Sie uns die Überladungen einführen, die sich mit Parametern in SQL-Abfragen und -Anweisungen befassen.

6.1. Zeichenfolgen mit Platzhaltern

In einem ähnlichen Stil wie bei normalem JDBC können wir Positionsparameter verwenden:

sql.execute(
    'INSERT INTO PROJECT (NAME, URL) VALUES (?, ?)',
    'tutorials', 'github.com/eugenp/tutorials')

oder wir können benannte Parameter mit einer Karte verwenden:

sql.execute(
    'INSERT INTO PROJECT (NAME, URL) VALUES (:name, :url)',
    [name: 'REST with Spring', url: 'github.com/eugenp/REST-With-Spring'])

Dies funktioniert fürexecute,executeUpdate,rows undeachRow. executeInsert unterstützt auch Parameter, aber die Signatur ist etwas anders und kniffliger.

6.2. Groovige Saiten

Wir können uns auch für einen Groovier-Stil mit GStrings mit Platzhaltern entscheiden.

Alle Methoden, die wir gesehen haben, ersetzen Platzhalter in GStrings nicht auf die übliche Weise. Sie fügen sie vielmehr als JDBC-Parameter ein und stellen so sicher, dass die SQL-Syntax korrekt beibehalten wird, ohne dass Anführungszeichen angegeben werden müssen, und daher besteht kein Risiko einer Injektion.

Das ist völlig in Ordnung, sicher und Groovy:

def name = 'REST with Spring'
def url = 'github.com/eugenp/REST-With-Spring'
sql.execute "INSERT INTO PROJECT (NAME, URL) VALUES (${name}, ${url})"

7. Transaktionen und Verbindungen

Bisher haben wir ein sehr wichtiges Anliegen übersprungen: Transaktionen.

Tatsächlich haben wir auch überhaupt nicht darüber gesprochen, wie GroovysSql Verbindungen verwaltet.

7.1. Kurzlebige Verbindungen

In den bisher vorgestellten Beispielen schließteach and every query or statement was sent to the database using a new, dedicated connection.Sql die Verbindung, sobald der Vorgang beendet wird.

Wenn wir einen Verbindungspool verwenden, kann dies natürlich nur geringe Auswirkungen auf die Leistung haben.

Dennoch,if we want to issue multiple DML statements and queries as a single, atomic operation, brauchen wir eine Transaktion.

Damit eine Transaktion überhaupt möglich ist, benötigen wir eine Verbindung, die mehrere Anweisungen und Abfragen umfasst.

7.2. Transaktionen mit einer zwischengespeicherten Verbindung

Groovy SQL erlaubt uns nicht, Transaktionen explizit zu erstellen oder darauf zuzugreifen.

Stattdessen verwenden wir diewithTransaction-Methode mit einem Abschluss:

sql.withTransaction {
    sql.execute """
        INSERT INTO PROJECT (NAME, URL)
        VALUES ('tutorials', 'github.com/eugenp/tutorials')
    """
    sql.execute """
        INSERT INTO PROJECT (NAME, URL)
        VALUES ('REST with Spring', 'github.com/eugenp/REST-With-Spring')
    """
}

Innerhalb des Abschlusses wird eine einzige Datenbankverbindung für alle Abfragen und Anweisungen verwendet.

Darüber hinaus wird die Transaktion automatisch festgeschrieben, wenn der Abschluss endet, es sei denn, sie wird aufgrund einer Ausnahme vorzeitig beendet.

Wir können die aktuelle Transaktion jedoch auch manuell mit Methoden in der KlasseSqlfestschreiben oder zurücksetzen:

sql.withTransaction {
    sql.execute """
        INSERT INTO PROJECT (NAME, URL)
        VALUES ('tutorials', 'github.com/eugenp/tutorials')
    """
    sql.commit()
    sql.execute """
        INSERT INTO PROJECT (NAME, URL)
        VALUES ('REST with Spring', 'github.com/eugenp/REST-With-Spring')
    """
    sql.rollback()
}

7.3. Zwischengespeicherte Verbindungen ohne Transaktion

Um eine Datenbankverbindung ohne die oben beschriebene Transaktionssemantik wiederzuverwenden, verwenden wir schließlichcacheConnection:

sql.cacheConnection {
    sql.execute """
        INSERT INTO PROJECT (NAME, URL)
        VALUES ('tutorials', 'github.com/eugenp/tutorials')
    """
    throw new Exception('This does not roll back')
}

8. Schlussfolgerungen und weiterführende Literatur

In diesem Artikel haben wir uns mit dem Groovy SQL-Modul befasst und wie es JDBC mit Abschlüssen und Groovy-Zeichenfolgen erweitert und vereinfacht.

Wir können dann sicher schließen, dass einfaches altes JDBC mit einer Prise Groovy ein bisschen moderner aussieht!

Wir haben nicht über jede einzelne Funktion von Groovy SQL gesprochen. Beispielsweise haben wirbatch processing, gespeicherte Prozeduren, Metadaten und andere Dinge weggelassen.

Weitere Informationen finden Sie unterthe Groovy documentation.

Die Implementierung all dieser Beispiele und Codefragmente finden Sie inthe GitHub project - dies ist ein Maven-Projekt, daher sollte es einfach zu importieren und auszuführen sein.