MicroStream: Speichern von Graphen und Datenstrukturen mit zykischen Verbindungen

© 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 XSequenceexportData(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!
Hinterlasse einen Kommentar