Eclipse-RCP-Anwendungsentwicklung mit Embedded Neo4j

Neo4j und Eclipse: Relationships are cool!

Lars Martin
© Shutterstock/Lichtmeister

NoSQL heißt das Zauberwort, wenn es um die Verarbeitung von Big Data geht. Die reine Größe von Daten ist hier allerdings nur ein Faktor. Zunehmend muss auch die Komplexität und die Konnektivität von Daten in die Definition einbezogen werden. Graphdatenbanken versprechen insbesondere für stark verknüpfte Daten ein ideales Speicherformat und Managementtool zu sein.

Mit dem nachfolgenden Artikel möchte ich dem Leser in Form eines Anwenderberichts einen Einblick in ein aktuelles Projekt geben, in dem wir seit einigen Monaten sehr intensiv eine Graphdatenbank im Einsatz haben – im konkreten Fall ist das Neo4j. Vielleicht führt dieser Artikel bei dem einen oder anderen Leser ja auch dazu, mal darüber nachzudenken, ob eine Graphdatenbank in seinem Projekt möglicherweise ein besseres Speichermedium ist als die relationalen Pendants, die oftmals noch verwendet werden. Sollte das der Fall sein, freue ich mich über eine kurze Nachricht dazu. Eine Einschätzung vorweg: Eine Graphdatenbank in diesem Projekt einzusetzen war definitiv eine richtige Entscheidung. Doch zunächst etwas zum Projektumfeld, damit das Nachfolgende besser einzuordnen ist.

Projektkontext

Gentoo Portage ist ein Sourcecode-basiertes Paketmanagementsystem, das von Gentoo Linux entwickelt wird und inzwischen auch auf anderen Plattformen verfügbar ist. Wenn ein „System“ erstellt wird, werden zunächst die Sourcen des bzw. der gewählten Pakete inkl. deren Compile Dependencies aus dem Source Repository ausgecheckt, für das jeweilige „System“ kompiliert und dann gemeinsam mit den benötigten Runtime Dependencies installiert. Dadurch entsteht ein optimal konfiguriertes System. Wie nun ein Paket zu bauen ist, wird in Ebuild-Dateien abgelegt. Ebuilds sind also eine Sammlung von Metadaten zu konkreten Softwarepaketen und werden als Portage Tree bezeichnet. Die Ebuild-Syntax entspricht der Bash-Syntax mit erweiterten Funktionen und Schlüsselwörtern. Nun erlaubt es die Flexibilität von Gentoo Portage, beliebig viele verschiedene solcher „Systeme“ zu verwalten. Was im Desktopbereich typischerweise individuell konfigurierte Linux-Systeme sind, sind in unserem Projektumfeld so genannte Firmware-Images, um damit die ebenfalls eigenentwickelte Hardware zu betreiben. Hier beispielhaft ein Ausschnitt aus der Ebuild-Datei des Pakets A aus der Kategorie develop in der Version 2.0:

# Auschnitt aus dem Ebuild "develop/A/A-2.0.ebuild"
...
DESCRIPTION="Description of Package A"
HOMEPAGE="http://www.domain.com/A"
SRC_URI="http://svn.domain.com/module/A"
DEPEND=">develop/B-1.0"
...

Die vollständige Spezifikation der Ebuild-Syntax ist hier zu finden. Um nun ein Gefühl für die Größenordnung im Projekt zu bekommen, hier ein paar aktuelle – und wie ich finde – sehr beeindruckende Zahlen: Derzeit sind im Portage Tree etwa 2 500 Pakete in verschiedenen Kategorien und Versionen enthalten. Insgesamt ergibt sich daraus ein Portage Tree mit ca. 100 000 physisch vorliegenden Ebuild-Dateien. Da ein Portage Tree in einer Ordner- und Dateihierarchie abgelegt wird, ist verständlich, dass bei dieser Anzahl von Dateien die tägliche Arbeit mit den Ebuild-Dateien eher einem „Suchen und Finden“ gleichkommt. Ein zu entwickelndes Werkzeug soll dabei die folgenden Funktionen bieten:

Editieren: Das Bearbeiten von einzelnen Ebuild-Daten soll durch einen Editor unterstützt werden. Insbesondere sollen dem Anwender mögliche Fehler angezeigt werden.
Navigieren, Validieren und Visualisieren: In den Ebuild-Dateien definierte Abhängigkeiten sollen zum einen navigierbar sein. Ohne manuell durch Ordnerhierarchien zu navigieren, soll der Anwender abhängige Ebuilds öffnen können. Dazu müssen dynamische Abhängigkeiten, wie etwa > 1.0, zunächst auf Basis des jeweils aktuellen Datenbestands ermittelt werden. Zum andern sollen fehlerhafte, also z. B. fehlende oder sich gegenseitig ausschließende, Abhängigkeiten kenntlich gemacht werden.

Editieren

In der Vergangenheit wurde im Eclipse Magazin bereits eine Vielzahl an Artikeln über die Erstellung von Xtext-basierten Editoren veröffentlicht. Deshalb möchte ich an dieser Stelle das Thema nur sehr kurz behandeln und den Fokus der folgenden Abschnitte auf die Themengebiete „Navigieren“ und „Visualisieren“ legen. Für die Ebuild-Syntax wurde im Rahmen des Projekts also ein Xtext-basierter Editor entwickelt, der zusätzlich durch einen formularbasierten Editor ergänzt wurde. Der Entwickler kann also je nach persönlichen Vorlieben den Text- oder Formulareditor verwenden. Syntax-Highlighting, Code Completion und Quickfixes seien hier der Vollständigkeit halber noch genannt.

Navigieren, Validieren und Visualisieren

Für das Navigieren durch den Abhängigkeitsgraph wurde zunächst ein Parser auf Basis einer weiteren Xtext-Grammatik entwickelt, der sich ausschließlich um das Parsen der DEPEND-Einträge in Ebuilds kümmert und den erstellten Abhängigkeitsgraph in einer Embedded-Neo4j-Datenbank ablegt. Da Neo4j derzeit nicht OSGi-konform ist, gilt es zunächst, Neo4j Eclipse-kompatibel zu machen. Der Kasten „Neo4j als Embedded-Datenbank im OSGi Container“ beschreibt ein mögliches Vorgehen bei der Integration von Neo4j als Embedded-Datenbank in Eclipse-Projekten.Abb. 1: Property-Graph einer geparsten Package- und Dependency-Struktur

Abbildung 1 verdeutlicht, dass in der Graphdatenbank zwei verschiedene Aspekte als Teilgraphen abgelegt werden: Zum einen wird die Paketstruktur einer Ebuild-Datei, bestehend aus Knoten für Package, Category und Version, abgelegt. Pro Paket im Portage Tree entsteht so ein separater Graph, der noch keine Verbindung zum Graphen eines anderen Pakets hat. Zum anderen werden die Abhängigkeiten einer Version von anderen Artefakten abgelegt. Mit diesen Abhängigkeiten werden die bisher einzelnen Graphen der Pakete miteinander verknüpft und bilden so den gesamten Dependency-Graphen des Portage Tree ab. Der vorliegende Portage Tree mit ca. 100 000 Ebuild-Dateien wird so in einen Graph mit ca. 300 000 Knoten und ca. 500 000 Relationen abgebildet.

Neo4j als Embedded-Datenbank im OSGi Container
Wer schon einmal versucht hat, mit Neo4j als Embedded-Datenbank in einer Eclipse-RCP-Anwendung zu arbeiten, hat vermutlich recht schnell festgestellt, dass Neo4j von Hause aus nicht (mehr) mit den notwendigen OSGi-Manifestdateien ausgeliefert wird. Überhaupt ist die OSGi-Unterstützung von Neo4j nicht auf dem aktuellen Stand, was vielleicht auch damit zu erklären ist, dass die Entwickler den Einsatz ihres Produkts eher als Standalone-Serverlösung denn als Embedded-Datenbank in einer Desktopanwendung sehen. Und die Pflege von OSGi-Metadaten erfordert auch einiges an Aufwand, insbesondere in einem extrem dynamischen Open-Source-Produkt wie Neo4j.

Selbst ist der Monolith – äh, Mann
Will man nun nicht alle Java-Archive aus der Neo4j-Distribution in seine RCP-Anwendung einbetten, bietet es sich an, eines der verfügbaren Third Party Bundles zu verwenden. Zum Beispiel steht hier ein Maven-Projekt zur Verfügung, das aus dem aktuellen Neo4j-Release ein OSGi Bundle erstellt. Dieses Bundle kann man dann in seine Eclipse-Zielplattform einbinden. Zwar verliert man mit diesem Ansatz ein paar Features, wie etwa zusätzliche Server Extensions, da diese manuell mit in das Bundle aufgenommen werden müssten. Aber immerhin kommt man mit diesem monolithischen Neo4j Bundle in den meisten Anwendungsfällen im Kontext von OSGi und Eclipse schnell ans Ziel. Es bleibt die Hoffnung, dass die Neo4j-Community irgendwann auch wieder echte OSGi Bundles herausgibt.

Hat man den Graphen erzeugt, die Knoten angelegt und durch Relationen verbunden, kann man z. B. über die Neo4j Shell ersten Kontakt mit dem Graphen aufnehmen. Jedoch will man in der Applikationsentwicklung nur begrenzt mit Shells interagieren. Auch das native Java-API von Neo4j macht hier nur kurze Zeit wirklich Spaß, da dieses API extrem „Low-Level“ ist und man lediglich mit Objekten vom Typ Node und Relationship arbeitet. Attribute an Objekten und Relationen sind einfache Key-Value-Paare. Von Data oder Application Modeling ist man damit sehr weit entfernt, auch wenn dieses API natürlich seine Daseinsberechtigung hat. Schaut man sich bei einer Suchmaschine seiner Wahl um, wird schnell klar, dass das Spring-Data-Projekt hier der Platzhirsch unter den Object Mappern ist (ORMs wie Hibernate seien hierbei vernachlässigt). Und mit Spring Data Neo4j gibt es auch eine Anbindung an Neo4j. An dieser Stelle möchte ich aber ein Object-Mapping-Framework namens EXtended Objects – kurz XO – vorstellen. Der Kasten „EXtended Objects – noch ein Object Mapper?“ gibt eine Kurzzusammenfassung zu den Funktionen, den Vor- und möglichen Nachteilen dieses Frameworks. Hier kann man sich jederzeit gern weiter informieren. Unter anderem die Leichtgewichtigkeit des Frameworks, insbesondere im Hinblick auf Eclipse als Zielplattform, hat für mich den Ausschlag gegeben, XO gegenüber Spring Data im Projekt den Vorzug zu geben. Listing 1 zeigt einen vereinfachten Ausschnitt des Domänenmodells aus dem vorgestellten Projekt. Vereinfacht deshalb, weil beim Lesen der Ebuild-Spezifikation vermutlich jedem schnell klar wird, dass es noch deutlich mehr Konzepte zu beachten gibt, die aber den Rahmen des Artikels sprengen würden.

@Label("DependencySource")
public interface DependencySource {
  public Set<Dependency> getDependencies();
}

@Label("DependencyTarget")
public interface DependencyTarget {
  public Set<Dependency> getDependents();
}

@Label("Version")
public interface Version extends DependencySource, Comparable<Version> {
  @NotNull
  String getValue();
  void setValue(String value);
  
  @Incoming
  @Relation("HAS_VERSION")
  Category getCategory();
  
  @ResultOf
  @Cypher("OPTIONAL MATCH (v:Version) WHERE (v:Head) and id(v)=id({this})" + "RETURN NOT(v IS NULL)")
  boolean isHead();
}

@Label("Head")
public interface Head extends Version {
}

@Label("Category")
public interface Category extends DependencyTarget, Comparable<Category> {
  @NotNull
  String getName();
  void setName(String name);
  
  @Outgoing
  @Relation("HAS_VERSION")
  Set<Version> getVersions();
}

@Label("Package")
public interface Package extends Comparable<Package> {
  @Indexed(unique = true)
  @NotNull
  String getName();
  void setName(String name);
  
  @Outgoing
  @Relation("HAS_CATEGORY")
  Set<Category> getCategories();
}

Aufmacherbild: Developing relationship concept: Construction machines building up with letters the word relationship, isolated on white background. von Shutterstock / Urheberrecht: Lichtmeister

[ header = Seite 2: Fazit ]

Vergleicht man den Property-Graph aus Abbildung 1 mit Listing 1, ist die Funktionsweise des XO-Frameworks leicht zu verstehen: Pro Typ (besser gesagt: pro Rolle) eines Knotens im Property-Graph wird ein Interface definiert und mit einer @Label-Annotation versehen. Durch die @Label-Annotation sind diese Rollen später z. B. in Queries typisiert abfragbar. Darüber hinaus kann man natürlich auch Interfaces ohne @Label-Annotation definieren, um z. B. gleiche Funktionen über verschiedene Typen hinweg zu teilen. Ein typisches Beispiel hierfür wäre ein Interface NamedElement mit getName()- und setName()-Mehoden, von dem dann alle entsprechenden Domänenobjekte erben. Interessant wird es bei der Definition der 1:n-Beziehungen zwischen Package und Category (ein Paket kann mehrere Kategorien haben) und zwischen Category und Version (in einer Kategorie sind typischerweise mehrere Versionen vorhanden). Hier bietet XO die Annotation @Relation, die optional noch einen Namen übergeben bekommen kann. Ist kein Name angegeben, wird standardmäßig der Methodenname als Relationsname verwendet – was in vielen Fällen sicher ausreichend ist. Zusätzlich ist die Richtung der Relation in vielen Anwendungsfällen relevant, weshalb man mit @Outgoing– und @Incoming-Annotationen diese Richtung konkretisiert. Da Relationen in Graphdatenbanken zunächst erstmal immer m:n-Beziehungen zueinander sind, muss mittels API die gewünschte Kardinalität definiert werden. Entsprechend liefert Category.getVersions() eine Collection<Version>, während Version.getCategory() genau ein Objekt vom Typ Category liefert.
Um zusätzlich dynamische Aspekte von Domänenobjekten abbilden zu können, bietet XO die Möglichkeit, eigene Queries an API-Methoden zu binden. Die Methode Version.isHead() führt also bei Aufruf durch die Applikation die annotierte Query aus und liefert deren Ergebnis als return-Wert der aufgerufenen Methode zurück. Neo4j bringt seine eigene Abfragesprache namens Cypher mit, die durch die gleichlautende @Cypher-Annotation an die Methode isHead() gebunden wird. Selbst für jemanden, der Cypher noch nicht kennt, sind Cypher-Queries meist recht leicht verständlich: Die in Listing 2 definierte Query sucht zunächst den aktuellen Kontextknoten (also den Knoten, der dem aktuellen Domänenobjekt entspricht, auf dem die Methode isHead() aufgerufen wird) im Graphen, um dort zu prüfen, ob dieser Knoten ein Label „Head“ hat. Je nach Vorhandensein dieses Labels wird true oder false zurückgeliefert. Wer weiter mit Cypher experimentieren möchte, dem lege ich die Cypher RefCard sowie die umfangreiche Dokumentation von Neo4j ans Herz.

@Relation("DEPENDENCY")
public interface Dependency {
  @Outgoing
  DependencySource getSource();
  
  @Incoming
  DependencyTarget getTarget();
  
  String getVersion;
  void setVersion(String version);
  
  Specifier getSpecifier();
  void setSpecifier(Specifier specifier);
}

Da die Definition einer Dependency weiterer Eigenschaften bedarf, nutzen wir an dieser Stelle getypte Relationen aus dem XO-Framework. Das heißt, die Relation hat nun den Typ Dependency, der wiederum durch ein eigenes Interface definiert wird. Diese Dependency spezifiziert nun neben der eingehenden und ausgehenden Kante zusätzliche Attribute für die geforderte Version und den Specifier. Als Specifier werden in der Ebuild-Spezifikation Vergleichsoperatoren für Versionen bezeichnet. Neben den bekannten Operatoren für >, >=, =, <=, < gibt es aber z. B. auch ~ (any) und „Ranges“ von Versionen. An solch getypte Relationen können nun beliebig viele Attribute angefügt werden, was auch der Definition eines Property-Graphen wie in Abbildung 1 entspricht. Ähnlich einer JPA Persistence Unit werden in einer Konfigurationsdatei namens xo.xml, die im Verzeichnis META-INF erwartet wird, die Persistenzeinstellungen deklariert. Listing 3 zeigt eine Konfigurationsdatei für die zuvor definierten Domänenobjekte unter Verwendung des Neo4j-Providers.

<v1:xo version="1.0" xmlns:v1="http://buschmais.com/xo/schema/v1.0">
  <xo-unit name="Neo4jEmbedded">
    <description></description>
    <url>file:/tmp/neo4jdb</url>
    <provider>com.buschmais.xo.neo4j.api.Neo4jXOProvider</provider>
    <types>
      <type>com.smbtec.xo.samples.ebuild.Category</type>
      <type>com.smbtec.xo.samples.ebuild.Dependency</type>
      <type>com.smbtec.xo.samples.ebuild.DependencySource</type>
      <type>com.smbtec.xo.samples.ebuild.DependencyTarget</type>
      <type>com.smbtec.xo.samples.ebuild.Package</type>
      <type>com.smbtec.xo.samples.ebuild.Head</type>
      <type>com.smbtec.xo.samples.ebuild.Project</type>
      <type>com.smbtec.xo.samples.ebuild.Version</type>
    </types>
    <validation-mode>AUTO</validation-mode>
    <concurrency-mode>MULTITHREADED</concurrency-mode>
    <default-transaction-attribute>REQUIRES</default-transaction-attribute>
  </xo-unit>
</v1:xo>

Dabei ist natürlich zu beachten, dass mit der Definition der Domänenobjekte die entsprechenden Packages, hier com.smbtec.xo.samples.ebuild, auch exportiert und somit vom Classloader des XO-Frameworks geladen werden können. Hier setzt XO im Fall von OSGi auf den BundleListener-Mechanismus. Wird also ein Bundle mit einer xo.xml in der OSGi Runtime registriert, wird durch XO für die darin enthaltene Persistenzkonfiguration ein Service registriert, den sich dann eine Applikation, z. B. durch einen Declarative Service, injizieren lassen kann.

eXtended Objects – noch ein Object Mapper?
Eigentlich gibt es doch schon genügend Object Mapper zur Persistierung von Domänenobjekten. Warum also noch ein Framework? Das eXtended-Objects-Framework – kurz XO – unterscheidet sich z. B. von Spring Data oder JPA maßgeblich dadurch, dass man mit XO seine Objekte in Form von Rollen definiert und nicht in statischen Entitäten. Diese Rollen von Objekten kann der Entwickler in seiner Applikation nun nahezu beliebig komponieren. An dieser Stelle kurz die wichtigsten Feature von XO:

• Sehr leichtgewichtig: das XO-Framework + Binding für Neo4j belegt weniger als 300 KB
• Anwendung bekannter Konzepte aus anderen Mapping-Frameworks wie etwa JPA und JDO
• Interfacebasierte Definition der Rollen von Domänenobjekten
• Statische Komposition von Domänenobjekten durch Mehrfachvererbung (Composite Pattern)
• Dynamische Komposition von Domänenobjekten zur Laufzeit durch Migration
• Unabhängig von konkreten Datenbanken und Speicherformaten
• Unterstützung von Transaktionen, Queries, Validierung

Inzwischen gibt es auch Bindings für weitere Datenbank-Backends wie etwa Titan, OrientDB und das TinkerPop-Blueprints-API. Sicher muss man durch die Implementierung des XO-Frameworks auf Basis von Dynamic Proxies und InvocationHandler einen gewissen Overhead bei der Laufzeit in Kauf nehmen. Ein Blick auf dieses noch junge Framework lohnt sich trotzdem auf jeden Fall.

Abschließend werfen wir in Abbildung 2 noch einen Blick auf die Visualisierung des Graphen im entwickelten Eclipse-Werkzeug. Eclipse Zest, basierend auf Eclipse GEF, bietet hier ein Toolkit zur Visualisierung von Graphen. Zwar sind die verschiedenen Layoutalgorithmen von Zest mit den teilweise sehr stark verknüpften Daten im Dependency-Graph manchmal etwas überfordert, eine optimale Darstellung zu finden. Aber es ist definitiv ein guter Ausgangspunkt, um z. B. gewisse Hotspots im Dependency Graph zu lokalisieren.

Abb. 2: Eclipse GEF/Zest zur Visualisierung des Dependency-Graphen

Fazit

Hat man einmal ein Projekt auf Basis eines graphbasierten Domänenmodells realisiert, sieht man quasi überall nur noch Graphen. Und tatsächlich ist es so, dass viele Datenstrukturen im (Programmierer-)Alltag eher einem Graphen als einer Tabelle ähneln. Dem Neo4j-Slogan „Graphs are everywhere“ kann ich nach meiner bisherigen Erfahrungen nur zustimmen. Insbesondere stark verknüpfte Daten, vielleicht sogar über mehrere Stufen hinweg, sind meist ein ideales Terrain für den Einsatz von Neo4j und Co. Hier können Graphdatenbanken ihren Vorteil durch die physisch vorhandenen Verknüpfungen zwischen den Domänenobjekten ausspielen, womit die Kosten zum Traversieren des Graphen in vielen Fällen vernachlässigbar gering werden. Relationale Datenbanken müssen diese Verknüpfung zum Abfragezeitpunkt immer erst durch einen JOIN berechnen. Und je mehr Stufen von Objekten verknüpft sind, umso mehr JOINs müssen berechnet werden. Hinzu kommt, dass man den Graphen nicht nur in eine Richtung traversieren kann, sondern sich beliebig über die Relationen hinweg bewegen kann. Das hat in dem zuvor beschriebenen Projektkontext „Dependency Management“ z. B. den netten Nebeneffekt, dass man sich nicht nur den Abhängigkeitsgraph eines Ebuilds anzeigen lassen kann, sondern – quasi auf Knopfdruck – auch die rücklaufenden Abhängigkeiten. Also alle Pakete, die über eine oder mehrere Stufen hinweg schließlich vom aktuell betrachteten Ebuild abhängig sind. Auf diese Art lassen sich problemlos Aufgabenstellungen aus dem Bereich Impact-Analyse bewerkstelligen. Generell kann man die Empfehlung geben, den Graphen mit möglichst vielen Informationen und Verknüpfungen anzureichern. Man wird dadurch sehr schnell völlig neue Erkenntnisse aus seinen Daten gewinnen können. Je nach Anwendungsfall lohnt es sich vielleicht auch, die eingesetzte Graphdatenbank mit anderen Speicherformaten und -medien zu kombinieren. „Polyglot Persistence“ ist hier das Stichwort. In Verbindung mit einem Object Mapper, wie dem vorgestellten Framework XO, und anderen Eclipse-Frameworks, wie dem verwendeten GEF/Zest, lassen sich Graphdatenbanken jedenfalls sehr leicht in die Eclipse-Applikationsentwicklung einbinden.
Der Artikel konnte die verschiedenen verwendeten Technologien nur streifen – weiterführende Informationen findet man in den unten verlinkten Internetressourcen. Fakt ist: Relationships are cool!

Geschrieben von
Lars Martin
Lars Martin
Lars Martin interessiert sich seit dem Abschluss seines Informatikstudiums 1998 insbesondere für Java-Technologien im Enterprise-Bereich. Im Rahmen seiner beruflichen Tätigkeit berät er heute Unternehmen bei der Verbesserung ihres Softwareentwicklungsprozesses.
Kommentare

Hinterlasse einen Kommentar

Hinterlasse den ersten Kommentar!

avatar
400
  Subscribe  
Benachrichtige mich zu: