H2 ist eine komplett in Java implementierte, relationale Open-Source-Datenbank, die man embedded, als Server- oder In-Memory-Datenbank verwenden kann. H2 eignet sich unter anderem in Prototypen, kleinen Services und für automatisierte Unit-Tests.

Moderne Ansätze zum Systemdesign wie Microservices oder Self-Contained-Systems bieten die Möglichkeit, einzelne Aspekte eines Gesamtsystems isoliert betrachten und bearbeiten zu können. Implementierung, Erweiterung und Deployment der einzelnen Komponenten sind durch automatisierte Tests schnell und trotz zahlreicher Build- und Deployment-Pipelines sicher. Die Vorteile der Isolation kommen vor allem in der Entwicklung zum Tragen, wenn es gelingt Services ohne weitere Umgebungskonfiguration auf der Entwicklermaschine aufzusetzen und testgetrieben entwickeln zu können. Für Services und Systeme, die eine relationale Datenbank als Datenquelle nutzen, gibt es für lokal und in Pipelines ausgeführte Unit-Tests unterschiedliche Ansätze zur Einbindung einer Datenquelle. In diesem Zusammenhang wird im Folgenden das relationale Datenbankmanagementsystem H2 vorgestellt, das insbesondere für Tests auch als In-Memory Lösung konfigurierbar ist.

Was ist H2?

H2 ist eine Open-Source-SQL-Datenbank, die komplett in Java implementiert ist. Sie lässt sich in drei Modi verwenden: Embedded, Server oder In-Memory. Im Embedded-Modus läuft die Datenbank in der gleichen JVM wie die Applikation und ist auch nur innerhalb dieser Virtual-Machine erreichbar. Dagegen kann die Datenbank im Servermodus auch von anderen JVMs aus angesprochen werden. Der In-Memory-Modus ist mit dem Embedded-Modus vergleichbar, jedoch mit dem wesentlichen Unterschied, dass der Zustand der Datenbank nicht über den Neustart der JVM persistiert wird. Die H2-Datenbank unterstützt Transaktionen und Basis-Konstrukte wie Triggers, Views und Check-Constraints. Darüber hinaus können bei der Verbindung Kompatibilitätsmodi für gängige SQL-Datenbanken konfiguriert werden, u.a. MySQL, Microsoft SQL-Server, PostgreSQL etc. Wird die Datenbank nicht im In-Memory-Modus betrieben, persistiert sie die Daten in Files, welche mittels AES-128 verschlüsselt werden können. H2 unterstützt JDBC, daher ist die Anbindung mit bewährten Standards wie JPA problemlos möglich. Die Projektwebseite1 bietet einen umfassenden Überblick über weitere Features sowie eine kurze tabellarische Übersicht mit einem Vergleich anderer Datenbanken.

Warum H2?

Als komplett in Java geschriebene Lösung lässt sich H2 per Dependency im entsprechenden Build-Tool konfigurieren und wird nach dem Klonen des Projekts auf der Entwicklermaschine automatisch beim ersten Bauen angezogen. Dadurch entfällt die Notwendigkeit, auf der Maschine eine manuelle Installation des Datenbanksystems vorzunehmen oder zumindest einen Container mit einer Installation hochzufahren. Insbesondere der In-Memory-Modus ermöglicht es, bei jedem Testlauf bzw. JVM-Start mit einem definierten Datenbankzustand zu starten. Dies dient vor allem der Reproduzierbarkeit automatisierter Tests. Durch die reine Integration als Dependency fügt sich H2 nahtlos in CI-/CD-Pipelines ein und es ist keine angepasste Konfiguration notwendig. Der schmale Footprint wirkt sich hier äußerst positiv auf Laufzeit und Ressourcenverbrauch aus.

Konfiguration für den In-Memory-Modus

Auf GitHub2 ist ein Projekt veröffentlicht, das ein komplettes Beispiel für die Konfiguration von H2 in einer Spring-Boot-Applikation bietet. Der Service stellt ein relativ einfaches REST-Interface zur Verwaltung von Tasks zur Verfügung. Für den Produktivbetrieb ist eine PostgreSQL-Datenbank vorgesehen und auch bereits unter src/main/resources/application.properties konfiguriert (Listing 1). Im Repository befindet sich eine Docker-Compose-Konfiguration, die einen Container mit einer Datenbankinstanz hochfährt. Diese wird verwendet, um die Applikation lokal laufen zu lassen.

(Listing 1)

Um die Unit-Tests nicht mehr gegen die PostgreSQL-Datenbank laufen zu lassen, sind zwei Schritte notwendig. Zunächst muss die Dependency in der Maven-Konfiguration eingebunden werden (Listing 2). Analog lässt sich auch Gradle konfigurieren.

(Listing 2)

Im zweiten Schritt wird eine bereits bestehende src/test/resources/application.properties erweitert oder die Datei angelegt. Im Wesentlichen müssen hier mindestens 4 Parameter überschrieben werden: Die URL der H2-Datenbank, Benutzername und Passwort sowie der zu verwendende Treiber (Listing 3). Dieser Treiber wird durch das Hinzufügen der Dependency im Build-File bereits angezogen. Der Einschub mem in der Datenbank-URL führt dazu, dass die Datenbank im In-Memory-Modus betrieben wird. Mit diesem Setup lassen sich die Unit-Tests im Optimalfall bereits gegen die H2-Datenbank ausführen.

(Listing 3)

Einbindung in Bestandscode

Gerade bei der Übernahme eines Bestandsprojekts kann die Notwendigkeit auftreten, das Projekt überhaupt erst lokal testbar zu machen. Die oben gezeigte Konfiguration ist hier ein eleganter Weg, dieses Ziel ohne umfangreiche Konfigurationen außerhalb des Projekts zu erreichen. Je nach Zustand des Bestandscodes können jedoch bei der Umsetzung diverse Stolpersteine auftreten. Würden beispielsweise bis dato keine Datenbankmigrationsskripte zur Schemagenerierung verwendet, müssen Tabellen und weitere Strukturen für die Testläufe auf anderem Wege angelegt werden. Als einfachster Ansatz ist zu prüfen, ob die Tabellenstruktur anhand von JPA-Entities angelegt werden kann. Hierzu kann der Konfigurationsparameter spring.jpa.hibernate.ddl-aut=create-drop in die Testkonfiguration aufgenommen werden. Sollte dieses Vorgehen keinen Erfolg bringen, besteht die Möglichkeit ein Datenbank-Migrations-Tool wie zum Beispiel Flyway3 für die Testläufe einzubinden. Hierfür wird dann ein einzelnes Migrationsskript angelegt, das die entsprechende Tabellenstruktur in H2 erstellt.

Ist für den Produktivbetrieb bereits ein Datenbank-Migrations-Tool konfiguriert, kann es zu dem Umstand kommen, dass einzelne Migrations-Skripte inkompatibel zu H2 sind. Wurden beispielsweise bisher herstellerspezifischer Dialekt oder Strukturen in den Skripten verwendet, kann der Kompatibilitätsmodus von H2 unter Umständen Abhilfe schaffen. In Fällen, in denen auch dieser Ansatz versagt, bleibt als letzte Möglichkeit einzelne Skripte in den Test-Resources zu überschreiben, diese also in Standard-SQL zu übersetzen. Diese können lokal und in Pipelines ausgeführt werden und sind schnell ein wertvoller Baustein zur Qualitätssicherung. Dies ist in der Gesamtbetrachtung in jedem Fall mit den für die Konfiguration anfallenden Aufwänden in Relation zu setzen.

Kritische Betrachtung

Nachdem nun die Fragen rund um Konfiguration und eventuelle Stolpersteine, kurz die technischen Möglichkeiten, geklärt sind, ist noch eine Betrachtung aus Sicht der nachhaltigen Softwarearchitektur notwendig. Insbesondere muss die Frage beantwortet werden, ob es vertretbar ist, für automatisierte Tests und in der Produktion unterschiedliche Datenbanksysteme zu verwenden. Pauschal ist dies sicherlich zu verneinen. Agile Methoden in der Softwareentwicklung zielen unter anderem darauf ab, die Zeit zu minimieren, in der Fehler auftreten. Ein Setup unterschiedlicher Datenbanksysteme kann dazu führen, dass ein spezifischer Effekt erst nach der Integration und nicht bereits beim lokalen Testlauf auftritt. In der Gesamtbetrachtung wird demgegenüber die Zeitersparnis durch die wegfallende, lokale Konfiguration und in den Pipelines gestellt. Im Kontext einer Microservice-Architektur kann die Entscheidung außerdem durchaus positiven Einfluss auf das Softwaredesign haben: Die parallele Verwendung zweier Datenbanksysteme zwingt zu der Verwendung von Standards bei der Entwicklung, beispielsweise die Verwendung bon SQL in Migrations-Skripten. Dies führt langfristig zu einer schwachen Kopplung des Microservices an das verwendete Datenbanksystem und erleichtert einen Umzug auf ein anderes System im Produktivbetrieb, sofern dafür in Zukunft die Notwendigkeit besteht. Zudem steigt die Fokussierung des Services auf die nach außen zur Verfügung gestellte Schnittstelle, wenn die Funktion von den technischen Möglichkeiten der dahinterliegenden Datenbank entkopppelt ist. Als Faustregel lässt sich folgendes formulieren: Je kleiner das Projekt und je mehr die Struktur der Daten anstelle von Abfrageperformance im Vordergrund stehen, desto eher bietet der Einsatz unterschiedlicher Datenbanksysteme für Produktivbetrieb und automatisierte Teste mehr Vor- als Nachteile. Letztlich muss für jede Applikation eine individuelle Antwort gefunden und begründet werden.

Die Grenzen von H2

Mit dem Aufkommen von NoSQL-Datenbanken hat sich der Markt für Datenbanksysteme wesentlich gewandelt. Relationale Datenbanksysteme punkten immer noch bei der Konsistenz, Transaktionssicherheit und Abfrageperformance in Anwendungsfällen, in denen viel Optimierungspotential in der Datenbank selbst ausgeschöpft werden kann. Wenn die Performance eine fachliche Anforderung an die Applikation ist, macht es Sinn, die Möglichkeiten des dahinterliegenden Systems vollends auszureizen. Hier ist vom Einsatz von H2 als Datenbank für Unit-Tests abzuraten, da andernfalls für einen Teil der Anforderungen keine testgetrieben Entwicklung möglich ist. Gleiches gilt, wenn durch fachliche Anfoderungen die Verwendung herstellerspezifischer Strukturen des verwendeten Datenbanksystems notwendig ist.

Kommt der Einsatz von H2 aus oben genannten Gründen nicht in Frage, bieten Testcontainer die erste Alternative. Mit dieser Technologie lassen sich ohne weitere externe Konfigurationen, Container mit einer spezifischen Datenbank wie beispielsweise PostgreSQL aus dem Java-Code heraus erstellen und als Datenquelle einbinden. Im Vergleich zur Konfiguration von H2 muss jedoch etwas mehr Aufwand betrieben werden. Insbesondere werden Java-Klassen erstellt, um den Testcontainer entsprechend aufzusetzen. Dies führt, wenn auch in überschaubarem Maße, zu höherer Komplexität im Projekt und einer größeren Code-Base.

Als letzte Lösung kann auch ein lokal zu startender Docker-Container dienen. Hierfür lässt sich ein Docker-File oder eine Docker-Compose-Konfiguration in das Repository einchecken. Die Konfiguration in diesem Fall bleibt ähnlich schlanck, wie bei der Verwendung von H2. Jedoch werden auf der Entwicklermaschinen zusätzlich installierte Komponenten vorausgesetzt und die Datenbank wird nicht ohne weiteres für jeden Testlauf auf einen leeren Zustand zurückgesetzt. Zudem erfordert dieser Ansatz auf jeder Entwicklungsmaschine und in jeder Pipeline zusätzlich Konfigurationsschritte.

Weitere Einsatzmöglichkeiten

Neben dem ausführlich vorgestellten Einsatz von H2 für automatisierte Tests gibt es noch weitere Szenarien, in denen sich die Verwendung anbietet. Für das Prototyping kann H2 seinen Vorteil des geringen Footprints voll ausspielen. In der ersten Phase der Entwicklung eines Produktprototyps wird hierbei auf die Konfiguration einer externen Datenbanklösung verzichtet. Diese kleine Ersparnis an Zeit und Komplexität mutlipliziert sich über die Anzahl der lokalen Entwicklungsumgebungen und die Deployment-Stages und ist mitunter nicht unerheblich. Durch die Verwendung von standardkonformen SQL und Abstraktions-Layern wie JPA ist beim Übergang eines Prototypen zur tatsächlichen Produktimplementierung einfach der Wechsel auf ein anderes Datenbanksystem möglich.

Andere sinnvolle Einsatzfelder sind Szenarien, in denen es auf eine schlanke Installation des gesamten Systems ankommt. Kleine Services, die im Wesentlichen von wenigen Nutzern verwendet werden, sind hierfür ein Beispiel. Durch den Wegfall einer separaten Datenbankinstallation mit den entsprechenden Betriebsaufgaben wie Updates, Backups etc. entfallen über die Lebensdauer der Applikation nicht unerhebliche Aufwände. H2 bringt sogar ein kleines Webinterface mit, das im Produktivbetrieb in einer Spring-Boot-Application per spring.h2.console.enabled=true aktiviert werden kann.

Fazit:

Das relationale Datenbankmanagementsystem H2 bietet sich durch seine schlanke Konfiguration und den schmalen Footprint zur Verwendung in Prototypen pder als Datenbank für Unit-Tests an. Das System lässt sich auch in Bestandsprojekten nachrüsten, in denen zuvor auf automatisierte Tests verzichtet wurde So wird in der entsprechenden Applikation nachhaltig die Qualität gesteigert. Durch die Verwendung von Standard SQL und Abstraktionsschichten in der Anbindung führt die Verwendung zu loser Kopplung der Applikation an das verwendete Datenbanksystem und zu einem insgesamt verbesserten Applikationsdesign.

[1] https://bit.ly/h2database

[2] https://bit.ly/project-g2

[3] https://bit.ly/flywaydb

 

Julius Mischok

Julius Mischok ist Geschäftshührer der Mischok GmbH in Augsburg. Seine Kernaufgaben sind Prozessentwicklung, sowie Coaching und Schulung der Entwicklungsteams. Aktuell fokussiert sich seine Arbeit auf die Fragen wie Software schnell und mit einer maximalen Wertschöpfung produziert werden kann. Er hat Mathematik studiert und entwickelt seit fast zwei Jahrzehnten Java.

E-Mail | Webseite | Xing

 

Redaktion


Leave a Reply