InspectorJ – Teil 3: MicroStream III

MicroStream: Speichern von Graphen und Datenstrukturen mit zykischen Verbindungen

Sven Ruppert

© Shutterstock / Sira Anamwong (modifiziert)

Das Java-Universum bietet viele Bibliotheken, Tools und Frameworks. In seiner Serie „InspectorJ“ untersucht Java-Experte Sven Ruppert aktuelle Trends. Im dritten Teil legt er sein Augenmerk nochmals eingehender auf das Projekt MicroStream. Das Framework verspricht, hochperformante Lösungen für die Serialisierung und Persistierung von Java-Anwendungen zu bieten. Diesmal geht es vor allem um Graphen.

Seit einiger Zeit gibt es einen neuen Mitbewerber im Bereich der Persistenz und Serialisierung. Die Rede ist vom Projekt MicroStream. Um was handelt es sich da genau? MicroStream nimmt in Anspruch, eine hochperformante und vor allem für den Entwickler sehr einfach zu handhabende Lösung für die Herausforderungen der Serialisierung und der Persistierung zu sein. In diesem Teil geht es um die Implementierung von Graphen.

Was bisher geschah

Im ersten Teil der Serie haben wir uns angesehen, wie wir ein Projekt für die MicroStream Engine vorbereiten, damit wir mit dem Experimentieren beginnen können. Im zweiten Teil ging es darum, ob man einen Baum genauso einfach wie eine einzelne Instanz einer Klasse handhaben kann, um diese auf der Platte zu persistieren. Im entsprechenden Beispiel handelte es sich um eine triviale Implementierung eines Binärbaumes.

Was heute ansteht

Nachem wir das letzte Mal einen Baum gespeichert haben, werde ich jetzt einen Graphen ausprobieren. Eine einfache Version davon ist ein Ring. Hierzu nehme ich wieder die im voherigen Teil erzeugte und verwendete Klasse mit dem Namen Node.

public class Node {
  public Node(int id) { this.id = id; }
  private final int  id;
  private       Node left;
  private       Node right;
  private       Node parent;
  //Getter - Setter - skipped
}	

Um nun einen Ring zu bilden, wird einfach damit begonnen, von einem Knoten immer das linke Kind zu setzen. Beim letzten Kind wird dann der erste verwendete Knoten als Kind eingesetzt. Die Frage, die sich hier stellt, ist demnach: Kann die MicroStream Engine mit solch einer Konstruktion umgehen, oder sind weitere Maßnahmen erforderlich, um mit Zyklen innerhalb einer zu speichernden Datenstruktur umzugehen?

final Node node01 = new Node(1);
final Node node02 = new Node(2);
final Node node03 = new Node(3);
final Node node04 = new Node(4);

node01.setLeft(node02);
node02.setLeft(node03);
node03.setLeft(node04);

node04.setLeft(node01);

Sobald diese Kontruktion erstellt worden ist, wird diese der MicroStream Engine mit dem Ziel übergeben, diese Instanz auf der Festplatte zu persistieren. Als Root-Node wird der Knoten mit der ID 1 verwendet.

final Instant start = now();
storageManagerA.setRoot(node01);
storageManagerA.storeRoot();
final Instant stop = now();
reporter.publishEntry("duration store [ms] " + between(start, stop).toMillis());
storageManagerA.shutdown();

Der Speichervorgang verläuft problemlos, was schon einmal ein gutes Zeichen ist. Somit scheint erkannt worden zu sein, dass alle Knoten nur einmal zu speichern sind. Als nächstes folgt nun der Versuch, diese Daten wieder in eine neu erzeugte Instanz einer MicroStream Engine zu laden.

final EmbeddedStorageManager storageManagerB = EmbeddedStorage.start(tempFolder);
final Node                   node01Again     = (Node) storageManagerB.root();

final Node node02Again = node01Again.getLeft();
final Node node03Again = node02Again.getLeft();
final Node node04Again = node03Again.getLeft();

Assertions.assertEquals(1, node01Again.getId());
Assertions.assertEquals(2, node02Again.getId());
Assertions.assertEquals(3, node03Again.getId());
Assertions.assertEquals(4, node04Again.getId());

Assertions.assertEquals(1,node04Again.getLeft().getId());

storageManagerB.shutdown();

In gewohnter Manier wird eine neue Instanz der Storage Engine erzeugt und der Root-Node geladen. Ein nachfolgender Durchlauf durch den Ring ergibt, dass es sich wieder um ein genaues Abbild der zuvor erzeugten Kontruktion handelt. Zugegebenermaßen ist das eine einfache Kontruktion gewesen, jedoch scheint es prinzipiell zu funktionieren.

Datenexport

An dieser Stelle angekommen, ergibt sich die interesante Frage, wie der Export von solch einer Datenstruktur aussehen möge. In der Dokumentation kann man nachlesen, dass es im Prinzip zwei Varianten gibt. Der erste und auch sicherlich auch einfachste Weg ist laut Dokumentation, einfach einen binären Dump zu erzeugen.

Binär-Version

Um nun den gerade erzeugten Ring als Backup in einem Binärformat abzulegen, wird auf jeden Fall ein Zielverzeichnis benötigt, in dem die Dateien abgelegt werden können. Damit ist schon mal der Ablageort definiert. Um nun die Daten aus der Engine zu bekommen, holen wir uns vom aktiven StorageManager, der die zu exportierenden Daten verwaltet, eine Verbindung vom Typ StorageConnection. Diese Verbindung kann man dann verwenden, um die Daten zu extrahieren und auf Festplatte zu exportieren.

Im Prinzip ist es nichts anderes als eine Kopie, mit dem Unterschied, dass diese im Vergleich zu dem regulären Datenverzeichnis ein wenig aufgeräumt worden ist. Ebenfalls scheint es der Fall zu sein, dass die einzelnen Dateien nun nach Typ sortiert sind. Soll bedeuten, dass in jeder Datei ein einzelner Typ persistiert worden ist. Die Dateinamen spiegeln das ebenfalls wieder. Einzelheiten über die Möglichkeiten sind allerdings in der Dokumentation noch nicht zu finden, was sich sicherlich in näherer Zukunft ändern wird.

private XSequence exportData(TestInfo info, EmbeddedStorageManager storageManagerA) {
  File targetDirectory = infoToCleanExportFolder("export_bin").apply(info);

  StorageConnection connection = storageManagerA.createConnection();
  StorageEntityTypeExportStatistics exportResult = connection.exportTypes(
      new StorageEntityTypeExportFileProvider.Default(targetDirectory, "bin"), 
    typeHandler -> true);
  // export all, customize if necessary
  return CQL.from(exportResult.typeStatistics().values())
            .project(s -> new File(s.file().identifier()))
            .execute();
}

CSV-Version

Wenn man nun einen binären Datenexport angefertigt hat, kann man diesen anschließend in ein CSV-Format konvertieren. Ein binärer Datenexport scheint demnach zwingend vorher durchzuführen zu sein. Um nun die Daten vom Binärformat in CSV zu konvertieren, muss man Zugriff auf alle erzeugten Dateien haben, die in dem Verzeichnis liegen, das für den Binär-Export verwendet worden ist. In diesem Beispiel ist es recht einfach, da der Binär-Export direkt vorher erfolgt ist und die jeweiligen Dateien für die persistierten Typen in einer Instanz vom Typ XSequence<File> vorhanden sind. Aus dieser Sequenz kann man dann alle Instanzen vom Typ File erhalten und dann nachfolgend für den CSV-Export verwenden.

private void exportDataAsCSV(EmbeddedStorageManager storageManager, 
                             File targetDirectory, File typeFile) {
  StorageDataConverterTypeBinaryToCsv converter = new StorageDataConverterTypeBinaryToCsv.UTF8(
      StorageDataConverterCsvConfiguration.defaultConfiguration(),
      new StorageEntityTypeConversionFileProvider.Default(targetDirectory, "csv"), storageManager.typeDictionary(),
      null,         // no type name mapping
      4096, // read buffer size
      4096  // write buffer size
  );
  StorageLockedFile storageFile = StorageLockedFile.openLockedFile(typeFile);
  try {
    converter.convertDataFile(storageFile);
  } finally {
    storageFile.close();
  }
}

Wenn man sich nun eine Datei im CSV-Export-Verzeichnis ansieht, erkennt man klar die Struktur, die hier zum Einsatz kommt.

;\;";\t;\n;{;};(;);/;/;*;*/
ObjectId left right parent id
(long reference reference reference int)
1000000000000000030 1000000000000000031 0 0 1
1000000000000000031 1000000000000000032 0 0 2
1000000000000000032 1000000000000000033 0 0 3
1000000000000000033 1000000000000000030 0 0 4

Ganz rechts erkennen wir die Dateninhalte wieder: die IDs von 1 bis 4. Anhand der Referenznummern kann man auch den Ring wiedererkennen. Das Format ist an dieser Stelle so simpel, dass man selbst von Hand weitere Datensätze einfügen kann.

Fazit

In diesem Teil haben wir zwei Dinge genauer angesehen. Zum einen ob es möglich ist, Graphen bzw. Datenstrukturen, die zyklische Verbindungen aufweisen, zu speichern. Diese wurden tatsächlich einwandfrei gespeichert und wieder in den Speicher eingelesen. Danach haben wir uns kurz angesehen, wie ein Export durchgeführt werden kann. Meine Motivation hierbei war zu sehen, wie die Daten imm CSV-Format abgelegt werden und ob ich daran den Ring, der vorher erzeugt worden ist, wiedererkennen kann. Das hat meines Erachtens gut funktioniert. Demnach kann man die Daten also in einem einfachen Textformat exportieren, was an sich ja schon ein wichtiges Kriterium für den späteren Einsatz darstellen kann.

Wer Fragen und Anmerkungen hat, meldet sich am besten per Twitter an @SvenRuppert oder direkt per Mail an sven.ruppert@gmail.com

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

Hinterlasse einen Kommentar

Hinterlasse den ersten Kommentar!

avatar
4000
  Subscribe  
Benachrichtige mich zu: