No-Database Database
Java In-Memory Database Jetstream

#JAVAPRO #JCON2017 #InMemoryDatabase #NoSQL

Mit Jetstream gibt es jetzt einen neuen Ansatz für die Persistierung von Daten in Java. Jetstream speichert Java-Objektgraphen genauso, wie diese im RAM existieren. Objekte müssen nicht durch Annotation oder XML-Konfigurationen aufwändig auf eine künstliche Struktur gemappt werden. Das In-Memory-Konzept von Jetstream ermöglicht Datenzugriffe im Bereich von Nanosekunden – 100.000 Mal schneller als HDD-Zugriffe relationaler Datenbanken.

Die Entwicklung von Datenbankanwendungen in Java gemäß dem Java-Standard JPA (Java-Persistence-API) ist ein komplexes Thema. Wer Datenbank-Applikationen für den professionellen Einsatz mit JPA entwickeln möchte, muss erfahrungsgemäß eine Menge Erfahrung im Umgang mit ORM-Frameworks wie Hibernate besitzen. Zudem erhöht sich die Komplexität einer Applikation mit dem Einsatz von JPA sprunghaft. Ganz besonders fällt das bei der Entwicklung von Microservices auf. Sobald man JPA verwenden möchte um Daten zu persistieren, explodiert der zuvor noch schlanke Programm-Code förmlich. Auch für mobile Geräte ist JPA zu schwergewichtig.

Mit Jetstream gibt es jetzt einen völlig neuen Ansatz für die Persistierung von Daten in Java, der

  • JPA überflüssig macht,
  • In-Memory Datenzugriffe ermöglicht, die im Bereich von Nanosekunden liegen – 100.000 Mal schneller als Festplattenzugriffe wie bei relationalen Datenbanken häufig der Fall,
  • die gesamte Datenbankentwicklung mit Java radikal vereinfacht und beschleunigt.

 

JPA sollte die Datenbankentwicklung mit Java eigentlich vereinfachen

Java stellt Daten bekanntlich in Form von Objekten, oder genauer in Form von Objektgraphen, dar. Dagegen sind fast alle „großen“ Datenbanksysteme wie MySQL relationale Datenbanken (RDBMS). Die Daten werden hier in zweidimensionalen Tabellen gespeichert, die über Relationen miteinander verknüpft sind.
Java und RDBMS sind demnach inkompatibel. Dieses Problem wird als Object-relational-impedance-mismatch[1] bezeichnet. Dazu kommen weitere Probleme:

  • Objektidentität – Objekte lassen sich eindeutig identifizieren (OID), Datensätze müssen dazu um einen künstlichen Primärschlüssel erweitert werden,
  • Vererbung und Verhalten – im relationalen Paradigma gibt es keinen vergleichbaren Ansatz,
  • Datenkapselung – in Objekten wird der der direkte Zugriff auf Attributwerte verhindert, in RDBMS lassen sich Datensätze direkt ändern,
  • Datenzugriffe – in Java kann man objektorientiert auf Daten zugreifen, auf RDBMS dagegen mit SQL. Als Abfrageergebnis erhält man kein Objekt, sondern ein flaches JDBC-Resultset.

Um objektorientiert auf relationale Datenbanken zugreifen zu können, wurden sogenannte ORM-Frameworks (Objekt-Relationales-Mapping) wie Hibernate entwickelt. Hibernate bildet eine Schicht zwischen der Java-Anwendung und der Datenbank, welche die darunterliegende Datenbank abstrahiert. Um das Mapping zwischen Objekten und Tabellen kümmert sich das Framework vollautomatisch. Im Hintergrund muss es eine Menge leisten, insbesondere:

  • für jeden Datenbankzugriff entsprechende SQL-Statements generieren und absetzen,
  • Java-Typen auf (häufig proprietäre) Datenbankdatentypen mappen (z.B. Java String <> SQL VARCHAR / CHAR) und umgekehrt,
  • aus Abfrageergebnissen Java-Objekte erzeugen u.V.m. Vor 10 Jahren wurde der objektrelationale Ansatz im JPA-Standard (Java-Persistence-API) standardisiert (JSR 220 [2]). Die Referenzimplementierung dazu war TopLink.

 

Nachteile von JPA

Ein großer Nachteil von JPA ist die Komplexität für den Entwickler. Für die Kombination von OOP und RDBMS müssen immer zwei Datenmodelle entwickelt und bei Änderungen entsprechend angepasst werden, was nicht nur aufwändig ist, sondern auch eine große Fehlerquelle darstellt. IDEs bieten zwar dafür Tools, trotzdem muss der Entwickler sehr viel manuell konfigurieren und später generierten Code händisch optimieren, was wiederum sehr fehleranfällig ist.
Zudem muss das Entity-Klassenmodell für JPA ausgelegt sein. Für die Umsetzung des OR-Mappings müssen die Klassen annotiert werden oder es bedarf einer XML-Konfigurationsdatei. Für die Abbildung von Vererbungsbeziehungen sind zusätzliche Tabellen nötig.

Ans Eingemachte geht es spätestens dann, wenn es um die Persistierung von Objekten selbst geht. Denn persistieren lassen sich nur Objekte, die an einen einen speziellen EntityManager gebunden sind (attached). Objekte, die dagegen detached sind, werden als normale Java-Objekte betrachtet und nicht mit der Datenbank synchronisiert. Auch bei der Persistierung selbst kann man sehr leicht fatale Fehler machen, u.a. bei der Wahl der richtigen Methode (flush, persist oder merge).

Ein Hauptnachteil von JPA ist die Performance. Permanentes OR-Mapping, Datatype-Mapping und Generieren von SQL-Statements ist teuer und kostet eine Menge Rechenzeit. Nur mit gutem Caching lässt sich dieser Zeitverlust kompensieren. Ohne Second-Level-Cache (z.B. EHCache) wäre Hibernate praktisch unbrauchbar. Deshalb sind JPA-Entwickler häufig mit Performance-Optimierungen durch Konfigurationsänderungen beschäftigt. Doch auch Caching ist komplex und gilt als eines der am meisten missverstandenen Konzepte von Hibernate.

Auch wenn es um Queries geht, wird mit JPA alles komplizierter. Zwar lassen sich mit JPA auch sogenannte native Queries in Form von SQL-Strings absetzen, aber dies hat gravierende Nachteile, u.a. sind diese nicht typsicher, es gibt keinerlei IDE-Unterstützung wie Refactoring, Warnings, Quickfixes etc. und der SQL-Code ist in der Praxis meist datenbankspezifisch. Deshalb stellt JPA mit JPQL und der JPA-Criteria API spezielle Query-APIs zur Verfügung. Während die eine (JPQL) ebenfalls Plain-Strings enthält und damit dieselben Nachteile wie native SQLs hat, ist die andere (Criteria) kompliziert und der Code wirkt aufgebläht.
Die Liste an Schwierigkeiten ließe ich hier noch weiter fortsetzen, denn mit den über JPA und insbesondere Hibernate verfassten Abhandlungen und Bücher ließe sich problemlos ein Wandregal füllen, was allein schon ein Indiz dafür ist, dass man es hier nicht mit einer simplen Thematik zu tun hat.

Jeder, der etwas tiefer in die Thematik einsteigt, muss sich zwangsläufig fragen: „Ich möchte doch lediglich Daten speichern. Geht das denn mit Java nicht auch einfacher?“

No-SQL

Moderne No-SQL Datenbanken versprechen schneller und einfacher zu sein, was sie natürlich auch für Java Entwickler interessant macht. Grundsätzlich kann man alle nicht-relationalen Datenbanken als No-SQL bezeichnen, grob unterscheidet man aber zwischen dokumentenorientierten-, spaltenorientierten-, Key-Value-, Graph- und älteren Objektdatenbanken. Die Hersteller positionieren sich meist gezielt, indem sie meist besonders geeignete Anwendungsfälle skizzieren.

Das wohl größte Problem bei No-SQL sind fehlende Standards und die bestehende Notwendigkeit ein zweites Datenmodell erstellen zu müssen. Damit benötigt man wie auch bei relationalen Datenbanken ein Mapping und muss seine Java-Anwendung speziell für die jeweilige Datenbank anpassen. Nur ist hier ein Datenbankwechsel aufgrund des fehlenden Standards wie JPA noch sehr viel problematischer als bei RDBMS. Hinzu kommen neue, proprietäre Abfragesprachen. Ob sich der Aufwand für die Entwickler deutlich reduziert ist fraglich.

Java-Objekte persistieren ohne Mapping

Mit Jetstream gibt es jetzt einen völlig neuen Ansatz für die Persistierung von Daten in Java. Jetstream ist eine in Java geschriebene Object-Storage-Engine, mit der sich beliebige Java-Objekte (POJO) aus dem Hauptspeicher auslesen, persistieren und umgekehrt jederzeit zurück in den Hauptspeicher laden kann.
An dieser Stelle ist wichtig zu erwähnen, dass Jetstream nicht vorschnell mit OO- oder Graphdatenbanken gleichgesetzt werden sollte, nur weil diese ebenfalls Objekte speichern können. Dies wäre wie das Gleichsetzen eines Pkw mit Verbrennungsmotor mit einem Elektroauto durch die Feststellung dass beide einen Motor besitzen. Auch im Falle von Jetstream sind die technischen Unterschiede gravierend, bis hin zu der Tatsache, dass Jetstream genau genommen gar keine Datenbank im klassischen Sinne ist. Aber der Reihe nach!

Einbinden von Jetstream

Jetstream ist eine nur 2,5 MB kleine Java-API, die man in jede Java-Anwendung via Maven einbinden kann:

Listing (1) – Maven repositories
<repository>
   <id>jetstream-releases</id>
      <url>http://maven.jetstream.one/repository/maven-releases/</url>
   <releases>
      <enabled>true</enabled>
   </releases>
   <snapshots>
      <enabled>false</enabled>
   </snapshots>
</repository>
<repository>
   <id>jetstream-snapshots</id>
      <url>http://maven.jetstream.one/repository/mavensnapshots/</url>
   <releases>
      <enabled>false</enabled>
   </releases>
   <snapshots>
      <enabled>true</enabled>
   </snapshots>
</repository>
Listing (2) – Maven dependency
<dependency>
   <groupId>one.jetstream</groupId>
   <artifactId>jetstream-one-core</artifactId>
   <version>0.8.0</version>
</dependency>

Architektur von Jetstream

Der größte Unterschied zu anderen Datenbanksystemen (und speziell zu Graph- und Objektdatenbanken) besteht darin, dass Jetstream kein autark laufender Datenbank-Server, sondern eine reine Storage-Engine ist – ähnlich wie InnoDB für MySQL. Jetstream selbst ist mit 2,5 MB eine vergleichsweise winzige Java-API, die man wie jede andere API als JAR direkt in seine Java-Anwendung einbindet. D.h., Jetstream ist ein fester Teil der Anwendung und läuft (embedded) zusammen mit der Anwendung in derselben Laufzeitumgebung. Die eigentliche Daten-Storage ist eine einfache Textdatei, die sich an einem beliebigen Ort befinden kann.

Funktionsprinzip

Eine Jetstream-Datenbank besteht jeweils aus einem (einzigen) Java-Objektgraphen, an dem alle Daten hängen. Das oberste Element wird als Root bezeichnet. Zur Laufzeit wird also zuerst eine Jetstream-Instanz erzeugt und danach das Root-Objekt.

Listing (3) – Instanziierung
Public class JetstreamDB extends JetstreamInstance<RootData> {

   private static JetstreamDB instance;

   private JetstreamDB() {
   }

   public static JetstreamDB instance() {
      if (null == instance) {
         instance = new JetstreamDB();
      }
      return instance;
   }

   @Override
   protected String createStorageDirectoryName() {
      return „my-database“;
   }
}

Anschließend lassen sich die Datenobjekte anhängen – und zwar tatsächlich völlig beliebige Java-Objekte. Der gesamte Objektgraph befindet sich dabei im Hauptspeicher.

Listing (4) – Beliebige Klassen persistierbar, z.B: Customer
public class Customer {

   private String firstname;
   private String lastname;
   private String mail;
   private Integer age;
   private Boolean active;

   ...
}
Listing (5) – Customers in der Klasse RootData einbinden
public class RootData {

   private final List<Customer> customers = new ArrayList<>();

   public List<Customer> getCustomers() {
      return this.customers;
   }

Persistierung

Durch den Aufruf der Methode storeRequired() werden alle am Objektgraphen durchgeführten Änderungen (Diffs) vollautomatisch persistiert. Die gezielte Persistierung einzelner Nodes ist ebenfalls möglich. Die für die Persistierung nötigen Objektinformationen werden aus dem RAM ausgelesen und (anhängend) in die File-Storage geschrieben. Der gesamte Vorgang ist automatisch transaktionssicher.

Listing (6) – Neuen Customer persistieren
final Customer customer = new Customer();

customer.setFirstname(„John“);
customer.setLastname(„Travolta“);
customer.setMail(„john.travolta@gamil.xy“);
customer.setAge(63);
customer.setActive(true);

final List&amp;amp;amp;amp;amp;lt;Customer&amp;amp;amp;amp;amp;gt; customersList = JetstreamDB.instance().
root().getCustomers();

customersList.add(customer);

JetstreamDB.instance().storeRequired(customersList);

JPA fällt vollständig weg

Da Jetstream Objekte direkt persistiert, ist JPA überflüssig. Ohne permanentes OR-Mapping ergibt sich ein wahrer Performance-Boost.

Daten löschen

Um Daten zu löschen, gibt es keine eigene Funktion, denn bei der Persistierung werden automatisch alle Objekte aus der Storage entfernt, die keine Referenz mehr zum Objektgraphen haben. Dafür besitzt Jetstream eine Cleaning-Funktion, die nach dem Prinzip des Garbage-Collectors funktioniert.

In-Memory Konzept

Beim Anwendungsstart wird der Objektgraph initial im RAM erzeugt. D.h., dass tatsächlich die gesamte Datenbank in den RAM geladen wird, falls möglich. Dies ist auch so beabsichtigt, denn dieses Konzept macht Jetstream zu einer Java-In-Memory-Datenbank, die den verfügbaren Hauptspeicher bestmöglich ausnutzt. Jetstream lässt jedoch auch zu, einzelne Nodes gezielt lazy zu laden, was ähnlich wie bei JPA funktioniert.

Keine Abfragesprache nötig

Da Jetstream den Objektgraphen, also die Datenbank, im Hauptspeicher hält, kann man direkt mit Java auf die Daten zugreifen, z.B. mit Hilfe der Java 8 Streams-API.

 

Listing (7) – Customer suchen
final List&amp;amp;amp;amp;amp;lt;Customer&amp;amp;amp;amp;amp;gt; result = JetstreamDB.instance().root().
getCustomers()

.stream()

.filter(customer -&amp;amp;amp;amp;amp;gt; customer.getFirstname().startsWith(„S“))

.collect(Collectors.toList());
//Ausgabe Konsole
result.forEach(customer -&amp;amp;amp;amp;amp;gt; {
   System.out.println(customer.getFirstname());
   System.out.println(customer.getLastname());
   //...
});

100.000 Mal schneller – tatsächlich möglich?

Um SQL-Queries abzuarbeiten, müssen RDBMS permanent auf den Festplattenspeicher zugreifen. Die mittleren Zugriffszeiten auf HDDs liegen bekanntlich im Bereich von Millisekunden. Zugriffe auf den RAM liegen dagegen im Bereich von Nanosekunden und sind damit tatsächlich 100.000 Mal schneller als Festplattenzugriffe. Komplexere Abfragen werden im schlechtesten Falle in Mikrosekunden abgearbeitet, also immer noch tausend Mal schneller. Dass diese Werte tatsächlich erreicht werden, dafür sorgt automatisch der JIT-Compiler (Just-In-Time) der JVM selbst.

Kein Netzwerk-Flaschenhals

Da die Daten einer Jetstream-Datenbank direkt im Hauptspeicher des Anwendungs-Servers liegen, müssen diese nicht mehr über ein Netzwerk transportiert werden. Der sogenannte Netzwerk-Flaschenhals sowie netzwerkbedingte Latenzzeiten entfallen ebenfalls.

Das Objektmodell ist das Datenmodell

Mit Jetstream gibt es nur noch ein einziges Datenmodell: das Java-Objektmodell. Es müssen weder Klassen annotiert, noch XML-Konfigurationen erstellt werden. Der Entwickler kann damit sein Objektmodell völlig individuell designen und muss keinerlei Rücksicht mehr auf die Persistenz nehmen. Änderungen und Erweiterungen sind damit jederzeit schnell und einfach durch ein Refactoring mit Hilfe der IDE erledigt.

Wo ist der Datenbank-Server?

Eine Datenbank bietet heutzutage u.a. eine Benutzerverwaltung, Import-/Export-Schnittstellen, kümmert sich um Nebenläufigkeiten (Sessions, Connections, Caching), ermöglicht das Auslagern von Business-Logik (Stored Procedures, Trigger, Constraints) und natürlich die Daten-Storage.
Im Zeitalter zweischichtiger-Architekturen waren diese Funktionen essentiell. In modernen Zweischichtarchitekturen werden die meisten dieser Funktionen vom Anwendungs-Server übernommen, weil man diese in Java sehr viel eleganter und besser implementieren kann (Klassen und Methoden, Webservices, Benutzerverwaltung, Objektgraphen, Concurrency etc.) und auf Grund individueller Anforderungen meist auch in Java implementieren muss.
Oft bekommt man den Eindruck, dass wir unsere Java-Programme um die Funktionen der Datenbank-Server herum bauen müssen, nur um die vergleichsweise mangelhaften Datenbankfunktionen benutzen zu können, was meist die Komplexität und den Entwicklungsaufwand unnötig erhöht.
Das einzige was uns in Java fehlt, ist die Möglichkeit unsere Objektgraphen auf einfache Weise zu persistieren. Deshalb beschränkt sich Jetstream ganz bewusst auf genau diese eine Funktion.

Migration auf Jetstream

Bereits bestehende JPA-Projekte lassen sich sehr leicht auf Jetstream umstellen. Das JPA-Entity-Klassenmodell kann man unverändert beibehalten. Vorhandene Annotationen sind unschädlich und können nachträglich entfernt werden. Für die Migration der Daten müssen diese lediglich einmalig in den Hauptspeicher geladen und mit Jetstream persistiert werden. Möchte man eine relationale Datenbank auf Jetstream portieren, für die noch keine Java-Klassen vorhanden sind, kann man sich diese mit Hilfe der Hibernate-Tools sämtliche Entity-Klassen durch Import generieren lassen.

Fazit:

Jetstream ist eine Java-Object-Storage-Engine, mit der sich beliebige Java-Objektgraphen völlig ohne ein Mapping persistieren lassen. JPA wird damit völlig überflüssig. Der Entwickler muss nur noch ein einziges Datenmodell, das Objektmodell erstellen und pflegen. Damit vereinfacht Jetstream die Datenbankentwicklung in Java radikal. Da Jetstream den Objektgraphen in den Hauptspeicher lädt, um den gesamten verfügbaren RAM auszunutzen, werden Abfragen direkt in Java formuliert und werden ebenfalls im Hauptspeicher ausgeführt. Dies ermöglicht enorme Zugriffsgeschwindigkeiten. Da Jetstream keine Server-Datenbank, sondern einer reine Storage-Engine ist, richtet sich Jetstream an Entwickler, die sich um die Benutzerverwaltung, Concurrency, Schnittstellen etc. lieber selbst kümmern und diese ohnehin in Java implementieren müssen. Mit einer Größe von nur 2,5 MB ist Jetstream vergleichsweise winzig und lässt sich problemlos in jede Anwendung einbinden. Jetstream ist unter www.jetstream.one verfügbar.


Autor – Sebastian Späth


Sebastian Späth ist Senior Java Developer und Technology Evangelist bei der XDEV Software Corperation.
Er verfügt über eine breite Berufserfahrung und war mit Rapiclipse an der Entwicklung einer eigenen Eclipse-Distribution beteiligt. Sebastians Schwerpunkte sind Rapid Devolpement und Rapid Prototyping.
LinkedINXING

 

(Visited 69 times, 1 visits today)

Leave a Reply