Suche
Teil 23: Backend meets Frontend: Trainer for kids 13

ORM mit Speedment

Sven Ruppert

© Shutterstock / HelenField

Der Kampf mit der Datenbank geht weiter. In dieser Ausgabe von „Backend meets Frontend“ schauen wir uns an, wie wir ORM-Werkzeuge möglichst einfach in unseren Entwicklungsprozess integrieren. Dazu nutzen wir das Tool Speedment, das auf Java 8 Streams setzt.

Das letzte Mal haben wir mit dem Thema Persistenz begonnen. Ich habe gezeigt, wie ein Test mit Plain-SQL geschrieben werden kann, der von einem jUnit-Test ausgeführt wird. Die Datenbank wurde für jeden einzelnen Test mit Flyway und TestContainers neu aufgesetzt. Nun ist es an der Zeit, sich anzusehen, wie wir ORM-Werkzeuge einsetzen können. Diese sollten sich möglichst komfortabel in die Entwicklung integrieren.

Zusätzlich zu den Quelltextbeispielen zu diesem Artikel verwende ich auch die Sourcen des Open-Source -Projekts Functional-Reactive. Die Sourcen befinden sich auf GitHub. Ebenfalls werde ich damit beginnen, funktionale Aspekte in die Entwicklung einfließen zu lassen. Hierzu stütze ich mich auf die Serie hier auf JAXenter unter dem Namen Checkpoint Java.

Wenn in einem Projekt die Frage nach der Persistenz aufkommt, werden die Diskussionen meistens
ein wenig anstrengend. Das liegt nicht zuletzt auch an persönlichen Interessen und Vorlieben. Ich möchte an dieser Stelle nicht den Anspruch erheben, eine allgemein gültige unumstößliche Antwort geben zu können. Wir werden uns heute einen Vertreter ansehen, der noch recht neu ist. Die Rede ist von Speedment.

Der große Unterschied von Speedment zu anderen ORM-Tools liegt darin, das Speedment einen Java 8 Stream auf die Daten liefert. In Kurzform bedeutet das, dass eine Tabelle oder das Ergebnis einer Abfrage nicht nur in Form einer Liste von Instanzen einer Klasse zur Verfügung stehen, sondern der Zugriff über das konsumieren des Streams realisiert wird.

Nehmen wir an, es gibt eine Tabelle, in der die Instanzen der Klasse Auto abgelegt worden sind. Der zugehörige Stream, der bei einer Anfrage auf diese Tabelle geliefert wird, ist dann vom Typ Stream. Um eine Teilmenge zu erhalten, wird die Methode filter des Streams verwendet. Das Framework ist dafür verantwortlich, dies in einem effizienten SQL-Konstrukt umzusetzen. Dazu ergeben sich natürlich einige Fragen: Wie wird das Mapping erzeugt? Wie wird das Datenmodell synchron zum Klassenmodell gehalten? Und wie wird das System initialisiert?

Im letzten Artikel haben wir die Instanzen der PostgreSQL mit dem Open-Source-Projekt TestContainers erzeugt. Wir benötigen aber eine laufende Instanz des verwendeten RDBMS, da noch nicht alles automatisiert worden ist. Außerdem brauchen wie laufende Instanzen, um Speedment die Möglichkeit zu geben, sich damit zu verbinden, um das Generieren der Quelltexte zu realisieren. Hier hilft uns wieder Docker, indem wir manuell einen PostgreSQL-Container starten und diesen verwenden.

docker run -it --rm --name postgres -p 5432:5432 postgres:latest 

Nun müssen wir noch die Konfiguration von Flyway derart erweitern, dass die Maven Targets ausgeführt werden können, um das Datenmodell automatisch zu erzeugen. In der pom.xml kann man die Verbindungsdaten für Flyway so definieren, dass die variablen Anteile durch ein Profil dynamisch gesetzt werden können, zum Beispiel für die IP oder den Port.

    <!-- inside properties section-->
    <flyway.url>jdbc:postgresql://${docker.host.ip}:5432/postgres</flyway.url>

  <profiles>
    <profile>
      <id>docker_host_localhost</id>
      <activation>
        <activeByDefault>true</activeByDefault>
      </activation>
      <properties>
        <docker.host.ip>127.0.0.1</docker.host.ip>
        <docker.host.port>2375</docker.host.port>
      </properties>
    </profile>
    <profile>
      <id>docker_host_sru_dev_machine</id>
      <activation>
        <activeByDefault>false</activeByDefault>
      </activation>
      <properties>
        <docker.host.ip>192.168.0.100</docker.host.ip>
        <docker.host.port>2375</docker.host.port>
      </properties>
    </profile>
  </profiles>

Hier habe ich nicht nur die eigene Maschine definiert, sondern auch einen älteren Rechner, auf dem eine Docker-Instanz läuft. So können wir auch gleich ausprobieren, ob es remote funktioniert. Wir sind nun in der Lage, den PostgreSQL-Container zu starten und das Flyway Maven Target auszuführen, um das Schema in der Datenbank zu erzeugen.

Backend meets Frontend

In der Artikelserie Backend meets Frontend stellt Sven Ruppert (Vaadin) Konzepte und Technologien rund um das UI-Framework Vaadin vor. Sein Fokus liegt dabei auf modernem Web-Design für Java-Backend-Entwickler.

Zum ersten Teil und damit dem Start der Tutorien rund um die UI-Entwicklung mit Java geht es hier entlang. Alle Teile der Serie Backend meets Frontend finden sich hier.

Speedment und JavaFX

Wie bei den meisten ORM Tools üblich, werden uns einige Werkzeuge mit an die Hand gegeben, um die notwendigen, allerdings meist auch langweiligen, Teile der Arbeit zu automatisieren. Die Rede ist vom Mapping. Auch hier wird der Weg gegangen, das ein Werkzeug die Struktur der Datenbank ausliest
und anhand der DDL die notwendigen Klassen generiert. Daraus ergeben sich einige Reibungspunkte. Ein Reibungspunkt ist, dass die DDL zuerst da sein muss, da alles darauf basiert. Es kann also nicht der Weg von der Klasse zur DDL gegangen werden.

Ich werde nicht auf alle Details der Modellierung eingehen. Wichtig ist nur zu wissen, das es ein Maven-Plug-in gibt, mit dem das Werkzeug gestartet werden kann. Dabei handelt es sich um eine JavaFX-Anwendung. Hier kommt der zweite Reibungspunkt. Denn jetzt brauchen wir JavaFX in unserem Projekt. Was bei einem Oracle JDK kein Problem ist, ist beim OpenJDK ein wenig umständlich. Hier fehlt JavaFX leider. Es muss also erst noch zur Verfügung gestellt werden. Ich muss allerdings gestehen, dass ich den einfachen Weg gegangen bin und einfach das Oracle JDK verwendet habe. Ich werde aber einen Weg zeigen, wie wir das in den meisten Fällen komplett umgehen können. Aber erst einmal der offizielle Weg.

Gehen wird davon aus, dass die Tabellen in der Datenbank definiert sind. In unserem Fall ist es eine einfache Tabelle, um die Aufgaben und Ergebnisse der Rechenaufgabe abzulegen.

CREATE SEQUENCE comp_math_basic_id_seq;
CREATE TABLE comp_math_basic (
  ID             SMALLINT PRIMARY KEY DEFAULT nextval('comp_math_basic_id_seq'),
  OP_A           FLOAT       NOT NULL,
  OP             VARCHAR(15) NOT NULL,
  OP_B           FLOAT       NOT NULL,
  RESULT_MACHINE FLOAT       NOT NULL,
  RESULT_HUMAN   VARCHAR(15),
  RESULT_OK      BOOLEAN     NOT NULL,
  CREATED        TIMESTAMP   NOT NULL
);
COMMIT;

Diese Tabelle wird mit Flyway angelegt. Nun kann Speedment diese Struktur auslesen und in eine Klasse und weitere interne Elemente übersetzen. Dazu wird das Maven Target mvn speedment:tool aufgerufen. Nachdem die JavaFX UI gestartet worden ist, kann man die notwendigen Parameter eingeben, damit sich das Werkzeug mit der Datenbank verbinden kann. Nachdem die Struktur aus der Datenbank ausgelesen worden ist, kann man noch einige projektspezifische Dinge anpassen. Wir werden allerdings nur die primären Daten anpassen. Damit ist gemeint, dass der Pfad für die generierten Quelltexte auf target/generated-sources/ gesetzt wird und der Paketname an das Projekt angepasst wird (Abb. 1). Wenn nun der Button zum Generieren der Quelltexte gedrückt wird, erhalten wir die für die Verwendung von Speedment benötigten Klassen in dem angegebenen Verzeichnis.

Abb. 1: Nachdem die Struktur aus der Datenbank ausgelesen worden ist, kann man noch einige projektspezifische Dinge anpassen

Als nächstes wird getestet, ob die generierten Quelltexte funktionstüchtig sind. Hierfür schreiben wir einen jUnit-Test. Dabei verwenden wir die Basisstrukturen, die wir im vorherigen Artikel erzeugt haben. Für jeden Test wird mit TestContainers eine Instanz der PostgreSQL gestartet und bringt das Schema auf den aktuellen Stand. Nun wird als dritter Schritt die Speedment-Applikation initialisiert und für den Test zur Verfügung gestellt. Diesen Initialisierungsvorgang kann man auch in eine Funktion auslagern. In diesem Fall kommen die Verbindungsdaten wieder aus dem Container selbst.

  public Function<JdbcDatabaseContainer, Public_Application> app() {
    return (container) -> new Public_ApplicationBuilder()
        .withPassword(container.getPassword())
        .withUsername(container.getUsername())
        .withConnectionUrl(container.getJdbcUrl())
        .build();
  }

Damit kann die Speedment-Applikation vor dem Start der Testmethode bereitgestellt werden.

  @BeforeEach
  void setUp() {
    postgreSQLContainer = ((CheckedSupplier<PostgreSQLContainer>) PostgreSQLContainer::new).get();
    postgreSQLContainer.ifPresentOrElse(
        GenericContainer::start,
        (Runnable) Assert::fail
    );
    postgreSQLContainer.ifPresent(c -> {
      final Flyway flyway = flyway().apply(c);
      flyway.clean();
      flyway.migrate();
    });

    postgreSQLContainer.ifPresent(c -> speedmentAppl = success(app().apply(c)));
  }

Zum Ende eines Tests wird die Speedment-Applikation wieder heruntergefahren.

  @AfterEach
  void tearDown() {
    speedmentAppl.ifPresent(Speedment::stop);
  }

Der Test an sich ist dann nur noch von dem API abhängig, das uns Speedment liefert. Im nachfolgenden Beispiel sehen wir uns an, ob die Anzahl der Ergebnisse in den Daten der Tabelle, die ein Ergebnis größer drei haben, in dreimal vorhanden sind.

  @Test
  void test002() {
    final Public_Application app = speedmentAppl.get();

    assertEquals(
        "amount of result > 3",
        3,
        app
            .getOrThrow(CompMathBasicManager.class)
            .stream()
            .filter(e -> e.getResultMachine() > 3)
            .collect(toList())
            .size()
    );
  }

Wir werden uns das API noch genauer ansehen. Hier ist erst einmal das Ziel, die Technologie selbst einzubinden.

Aktualisieren der Schemata

Damit haben wir das aktuelle Schema in der Datenbank inklusive der Testdaten und ein dazu passendes
Abbild in Speedment. Wie gehen wir nun mit Änderungen um? Der Vorgang ist recht geradlinig, steht doch die Reihenfolge der zu verwendenden Werkzeuge fest. Zuerst starten wir den PostgreSQL-Container, dann verwenden wir Flyway zum Erzeugen der aktuellen Struktur in der DB und generieren die Speedment-Quelltexte. Die beiden letzten Teile lassen sich einfach mit Maven umsetzen.

      <plugin>
        <groupId>org.flywaydb</groupId>
        <artifactId>flyway-maven-plugin</artifactId>
        <configuration>

        </configuration>
        <executions>
          <execution>
            <id>flyway :: migrate</id>
            <phase>generate-sources</phase>
            <goals>
              <goal>migrate</goal>
            </goals>
          </execution>
        </executions>
      </plugin>
      <plugin>
        <groupId>com.speedment</groupId>
        <artifactId>speedment-maven-plugin</artifactId>
        <dependencies>
          <dependency>
            <groupId>org.postgresql</groupId>
            <artifactId>postgresql</artifactId>
            <version>${postgresql.version}</version>
          </dependency>
        </dependencies>
        <configuration>
          <dbmsHost>${docker.host.ip}</dbmsHost>
          <dbmsPort>5432</dbmsPort>
          <dbmsUsername>${flyway.user}</dbmsUsername>
          <dbmsPassword>${flyway.password}</dbmsPassword>
          <!--<dbmsType>postgres</dbmsType>-->
        </configuration>
        <executions>
          <execution>
            <id>speedment :: clear</id>
            <phase>generate-sources</phase>
            <goals>
              <goal>clear</goal>
              <goal>reload</goal>
            </goals>
          </execution>
          <execution>
            <id>speedment :: generate</id>
            <phase>generate-sources</phase>
            <goals>
              <goal>generate</goal>
            </goals>
          </execution>
        </executions>
      </plugin>

Nur der erste Teil ist ein wenig umständlich, wenn man mit Maven einen Docker-Container starten möchte. Es gibt verschiedene Docker-Plug-ins für Maven. Allerdings wollte keines der Plug-ins ordentlich aufräumen, wenn es zu einem Fehler bei einem mvn clean install gekommen ist. Aus diesem Grund habe ich mich dazu entschieden, das Beispiel einfach zu halten und den einen Aufruf
manuell auszuführen. Diese PostgreSQL-Instanz wird ausschließlich zum Generieren der Speedment-Quelltexte verwendet.

Generieren mit Minimal JSON

Wie kann man aber ganz ohne JavaFX auskommen, wenn das OpenJDK verwendet werden soll? Man kann sich OpenFX holen und installieren. Aber das ist ein Zusatzaufwand, den ich gerne vermeiden würde. Außerdem soll der Prozess auch vollständig in einer CI-Umgebung laufen können. Demnach wäre eine Variante ganz ohne die Verwendung einer UI und ohne manuelle Eingriffe erstrebenswert.

Wenn man sich das Projekt ein wenig genauer ansieht, findet man eine Konfigurationsdatei, die durch das Tool angelegt worden ist. Dort sind einige projektspezifischen Dinge inklusive aller Mapping-Informationen hinterlegt. Versuche haben ergeben, dass es ausreicht, wenn die Datei lediglich aus dem Grundgerüst besteht; unter der Annahme, dass ein voll automatisiertes Mapping funktioniert. Das bedeutet, dass alle Informationen zu den Tabellen selbst entfernt werden können. Dadurch haben wir ein Template in JSON, das wir für unsere Projekte verwenden können. Das Maven Target speedment:generate erzeugt dann wieder alle Tabelleneinträge. Das Attribut appId kann einfach so bleiben.

{
  "config" : {
    "expanded" : true,
    "appId" : "xxxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxx",
    "companyName" : "RapidPM",
    "name" : "Vaadin",
    "packageLocation" : "target/generated-sources/",
    "id" : "vaadin",
    "packageName" : "org.rapidpm.vaadin.trainer.persistence.speedment",
    "dbmses" : [
      {
        "expanded" : true,
        "port" : 5432,
        "schemas" : [
          {
            "expanded" : true,
            "tables" : [

            ],
            "name" : "public",
            "id" : "public",
            "enabled" : true
          }
        ],
        "typeName" : "PostgreSQL",
        "ipAddress" : "192.168.0.100",
        "name" : "postgres",
        "id" : "postgres",
        "enabled" : true,
        "username" : "postgres"
      }
    ],
    "enabled" : true
  }
}

Fazit

Wir haben nun alles zusammen, um kompakt Anfragen mit Speedment an eine RDBMS-Instanz zu senden und auszuwerten. Die Unterstützung durch die Entwicklungswerkzeuge ist sehr gut und unsere Variante kann auch auf CI-Servern direkt zum Einsatz kommen. Im nächsten Teil dieser Serie werden wir uns ausgiebig damit beschäftigen, wie wir Vaadin an dieses Persistenz-Layer anbinden können. Den Quelltext findet ihr auf GitHub. Bei Fragen und Anregungen einfach melden unter sven@vaadin.com oder per Twitter @SvenRuppert.

Happy Coding!

Geschrieben von
Sven Ruppert
Sven Ruppert
Sven Ruppert arbeitet seit 1996 mit Java und ist Developer Advocate bei Vaadin. In seiner Freizeit spricht er auf internationalen und nationalen Konferenzen, schreibt für IT-Magazine und für Tech-Portale. Twitter: @SvenRuppert
Kommentare

Schreibe einen Kommentar

Deine E-Mail-Adresse wird nicht veröffentlicht.