Über unsMediaKontaktImpressum
Stefan Schubert 13. August 2019

Einstieg in die Graphdatenbank Neo4j

Wir befinden uns in einer Zeit der sozialen Netzwerke, künstlicher Intelligenzen und Sprachassistenten. Wir haben Smartphones, die uns erlauben, eine grenzenlose Vernetzung untereinander herzustellen und besitzen im Schnitt zehn internetfähige Geräte pro Haushalt [1]. Dieser Wandel ist der Hauptgrund für die Notwendigkeit von Big-Data-Technologien, jedoch zieht er nicht nur immer größer werdende Datenmengen nach sich, sondern auch immer stärker zusammenhängende Daten, da die tiefergehenden Informationen oft in den Verbindungen der Daten liegen, statt in den Daten selbst.

So liegt beispielsweise der Fokus von sozialen Netzwerken nicht auf den Informationen bezüglich eines Individuums, sondern auf den Beziehungen zwischen den Personen. Wer arbeitet mit wem zusammen, wer kennt wen über welches Medium, welche Interessen verbinden zwei Menschen? Dies sind einige der Kernfragen, für deren Antwort Entwickler von sozialen Netzwerken Algorithmen implementieren müssen, was jedoch im Rahmen einer relationalen Datenbank oft alles andere als trivial ist. In diesem Kontext wollen wir einen Blick auf die Graphdatenbanken – speziell Neo4j – werfen.

Abgrenzung

Betrachten wir erneut die sozialen Netzwerke, so ist beispielsweise die Frage nach der Verbindung zwischen zwei beliebigen Personen ein relevantes Problem. In einer gängigen relationalen Datenbank würde man die Personen in einer gemeinsamen Tabelle speichern und die Verbindung zwischen einzelnen Personen in einer anderen. Sucht man auf diesem Wege nach Beziehungen zwischen zwei Personen, die eine direkte Verbindung haben, fügt man die Personen-Tabelle und die Beziehungs-Tabelle zusammen und vereint das Resultat erneut mit der Personen-Tabelle (Abb. 1).

Haben die Personen keine direkte Verbindung, muss das Resultat dieser beiden Joins erneut mit der Beziehungs-Tabelle und der Personen-Tabelle gejoint werden. Diese Prozess wird solange wiederholt, bis die gewünschten Personen in einem der Kreuzprodukte auftauchen. Besonders bei vielen Einträgen kann dieser Vorgang bei einer relationalen Datenbank sehr lange dauern.

Eine Vorgehensweise, die erlaubt, den Umgang mit solchen Problemstellungen stark zu vereinfachen ist die Verwendung von Graphdatenbanken. Hierbei handelt es sich um NoSQL-Datenbanken, welche Daten in einer Graphstruktur anstatt der bekannten Tabellenstruktur speichern. Insbesondere werden hierbei Relationen explizit als Objekte persistiert, die in einer relationalen Datenbank implizit durch Fremdschlüssel abgelegt würden. Hierdurch müssen, wie bei allen NoSQL-Datenbanken, keine Joins mehr gebildet werden, da die verknüpften Entitäten direkt über eine Kante der ursprünglichen Entität erreichbar sind.

Auf diesem Wege können wir beispielsweise die Frage, was zwei beliebige Personen verbindet, einfach lösen, indem wir den kürzesten Weg von einer Person zur anderen suchen. Eine Problemstellung, die durch Graphalgorithmen, wie beispielsweise den Dijkstra-Algorithmus schnell gelöst ist [2]. Dies beschleunigt die Lesegeschwindigkeit deutlich, da diese nicht mehr proportional zu der Menge der Daten ist, sondern zu der Tiefe in der sie durchsucht werden – eine Eigenschaft, die besonders im Big-Data-Umfeld nützlich ist.

Ein Nachteil dieses Vorgehens liegt jedoch in der Tatsache, dass Relationen persistente Objekte sind. Wo eine Zeile mit zehn Fremdschlüsseln in einer relationalen Datenbank als ein Objekt geschrieben wird, sind es in einer Graphdatenbank elf. Eins für das Objekt, zehn für die Relationen. Dadurch sind Graphdatenbanken im Allgemeinen nicht so performant wie eine relationale Datenbank, wenn es um die Schreibgeschwindigkeit geht.

Variationen Graph-DBs

Es existieren viele NoSQL-Datenbanken, die sich als Graphdatenbanken kategorisieren lassen, so z. B. die ArangoDB, die OrientDB, AWS Neptune und auch die oben genannte Neo4j. Hierbei wollen wir zwischen nativen und nicht-nativen Graphdatenbanken unterscheiden. Nicht-nativ bedeutet, dass die Datenbank eigentlich eine relationale Datenbank ist, die auf Graphen optimiert ist [3] – die nativen Graphdatenbanken speichern die Daten jedoch wirklich als Graph.

Wir wollen uns auf Neo4j als den Marktführer unter den Vertretern der nativen Graphdatenbanken konzentrieren. Neo4j ist eine Java-Implementierung der Graphdatenbank, welche alle ACID-Eigenschaften erfüllt und die Daten als einen sogenannten Label-Property-Graph persisiert. Das bedeutet, dass, abgesehen von der Tatsache, dass Daten als Knoten und Kanten abgelegt werden, diese Knoten und Kanten mit Labels gekennzeichnet werden, die ausdrücken, welche Art von Entitäten sie darstellen. Knoten können dabei beliebig viele Labels besitzen. Weiterhin können Knoten und Kanten Attribute besitzen.

Wie die meisten gängigen Applikationen kann auch die Neo4j-Datenbank alleinstehend oder in einem Container deployt werden und unterstützt in der Enterprise-Version gängige Features wie Metric- und Health-Endpoints, sowie Clustering. Für das bessere Verständnis der Nutzung dieser Datenbank wollen wir beispielhaft Daten aus einer relationalen Datenbank in einen Graph überführen (Abb. 2).

Wir wollen Entitäten in der Regel als Knoten ablegen, deshalb übersetzen wir jede Zeile der Personen-Tabelle in einen Knoten, der als Person gelabelt ist. Wir überführen jede nicht-Fremdschlüssel-Spalte der Personen-Tabelle in ein Attribut der korrespondierenden, als Person gelabelten Knoten. Zuletzt legen wir jede Fremdschlüssel-Beziehung zwischen zwei Entitäten als eine Kante zwischen den entsprechenden Knoten ab.

Cypher

Neo4j wird mit einem eigenen Client ausgeliefert, der erlaubt, manuelle Anfragen gegen die Datenbank abzusetzen (Abb. 3).

Alle Anfragen an die Datenbank werden in Cypher geschrieben, Neo4j's hauseigener, deklarativer Query-Language. Cypher funktioniert mittels Matching von Mustern, die in ASCII-Notation angegeben werden – man zeichnet quasi die Muster, mit denen man interagieren will. Wie auch SQL wird Cypher für jede Art der Interaktion mit der Datenbank genutzt, wie das Anlegen von Indexierungen, den Import und Export von Daten, Recovery sowie Queries.

Wollen wir in einem sozialen Netzwerk beispielsweise fragen, ob eine bestimmte Person Anna eine bestimmte andere Person Sven mag, ist dies gleichbeutend zu Existenz des Resultates des Befehls "Finde das Muster Anna mag Sven". Dieses Muster entspricht einem Knoten mit Label Person mit dem Attribute name='Anna' mit einer Relation des Types LIKES auf einen Knoten mit dem Label Person und dem Attribut name='Sven' (s. Listing 1).

Listing 1: Der Cypher-Befehl für das Finden des Musters "Anna mag Sven"

$ MATCH (a:Person {name:'Anna'})-[:LIKES]->(s:Person {name:'Sven']) return a,s;

Zum Finden des Musters nutzen wir den MATCH-Befehl. Zum Anlegen des Musters – unabhängig davon, ob es evtl. schon existiert – nutzen wir CREATE. MERGE ist eine Kombination aus MATCH und CREATE. Dieser Befehl sucht das Muster und legt es an, falls es nicht gefunden wird. SET wird genutzt, um Eigenschaften von Mustern zu ändern. DELETE können wir verwenden, um Muster zu löschen. Abgehend von diesen Basis-Funktionen bringt Cypher eine große Auswahl weiterer Funktionalitäten, wie z. B. String-Concatenation, List-Operations, mathematische Funktionen, usw. Diese sind vollständig in der Cypher-Refcard zu finden [4]. Cypher lädt die Daten tabellarisch (Abb. 4), zeigt sie im Client aber graphisch an (Abb. 5).

Auf diesem Wege werden die Daten leicht lesbar dargestellt und es können in Cypher geschulte Mitarbeiter den Neo4j-Client direkt als Tool für graphische Auswertungen von Daten verwenden.

Plugins

Wie auch andere Datenbanken lässt sich eine Neo4j durch Plugins, bzw. sogenannte Functions und Procedures erweitern.

  • Hierbei sind Functions Hilfsfunktionen, die Cypher erweitern, selbst aber keinen direkten Zugriff auf die Datenbank haben. Functions bekommen Werte übergeben und geben einen einzigen Wert zurück. Ein Beispiel hierfür ist String-Concatenation, eine Function, die eine Liste von Strings übergeben bekommt und einen einzigen String zurückgibt.
  • Procedures sind mächtigere Funktionen, die im Gegensatz zu Functions zusätzlich Zugriff auf die Datenbank besitzen und Streams von Werten zurückgeben. Diese können unter anderem genutzt werden, um komplett neue Funktionen zu implementieren, wie z. B. eine TTL für Knoten oder um komplexe Cypher-Befehle in einen einzelnen zu verpacken.

Functions und Procedures werden unter Verwendung des Neo4j-Kernels in Java implementiert und als jar im Plugin-Folder der Datenbank abgelegt. Von dort aus werden sie durch einen Neustart der Datenbank automatisch deployt. Abgesehen von der Möglichkeit, eigene Plugins zu implementieren, bietet Neo4j die Plugin-Library APOC (Awesome Procedures On Cypher) an, die über 450 Functions und Procedures enthält [5]. Weiterhin pflegt Neo4j die Plugin-Library Neo4j-Graph-Algorithms, die über 30 Graph-Algorithmen enthält, wie beispielsweise A*, Betweenness Centrality und PageRank [6].

OGM

Für die Arbeit mit Neo4j ist OGM ein wichtiges Werkzeug [7]. Kurz für Object-Graph-Mapping handelt es sich dabei um eine Library, die die Abbildung von Java-Objektstrukturen auf die Neo4j-Graphstruktur übernimmt, sowie den programmatischen Zugriff auf die Datenbank über eine Session, welche die CRUD-Funktionen liefert. Damit entspricht OGM dem, was Hibernate für relationale Datenbanken darstellt. Das Mapping ist simpel gehalten und ähnlich zu Hibernate über Annotations gelöst.

Sollen Objekte einer Klasse auf Knoten gemappt werden, wird die Klasse mit @NodeEntity annotiert. Attribute die nicht transient sind, werden automatisch gemappt und eingebettete Objekte können mit @Relationship annotiert werden, um das Mapping auf eine Relation umzusetzen (s. Listing 2).

Listing 2: OGM-Annotationen für eine Person, die mehrere Personen kennen kann

@Data
@NodeEntity
public class Person {

    @Id
    @GeneratedValue
    private Long id;

    private String name;

    @Relationship(type = "KNOWS")
    private List<Person> persons;

}

Weiterhin lassen sich Klassen mit @RelationshipEntity annotieren, um eine Klasse auf eine Relation mit zusätzlichen Attributen zu mappen. Diese Klasse benötigt jeweils einen gekennzeichneten @StartNode und @EndNode (s. Listing 3) und muss entweder im Start- oder Endknoten referenziert werden, um gemappt zu werden (Listing 4).

Listing 3: Eine auf eine Klasse gemappte Relation mit zusätzlichen Attributen

@Data
@RelationshipEntity(type = „KNOWS“)
public class KnowsRelation {

    @Id
    @GeneratedValue
    private Long id;

    private Instant since;

    @StartNode
    private Person from;

    @EndNode
    private Person to;

}

Listing 4: Eine referenzierte RelationshipEntity

@Data
@NodeEntity
public class Person {

    @Id
    @GeneratedValue
    private Long id;

    private String name;;

    @Relationship(type = „KNOWS“)
    private List<KnowsRelation> knowsRelations;

}

Aus technischer Sicht benötigen alle Entitäten eine Id, die mit @Id gekennzeichnet wird und die wir uns generieren lassen können, indem wir sie mit @GeneratedValue annotieren. Hierbei können wir zusätzlich eine IdStrategy mit angeben, die spezifiziert, wie die Ids generiert werden. Wird keine angegeben, verwendet OGM die nativen Ids, welche von der Datenbank generiert werden. Dies ist jedoch nicht empfohlen, da diese Ids von der Datenbank nach einiger Zeit recycelt werden. Dies kann bei langlaufenden Applikationen zu Problemen führen. Daher wird empfohlen, eigene Ids zu erzeugen, indem beispielsweise die UuidStrategy genutzt wird, um Uuids zu generieren.

Da die Neo4j-Datenbank aber – wie die meisten Datenbanken –, nicht in der Lage ist, nicht-triviale Werte zu speichern, benötigen wir einen Mechanismus, der komplexere Typen, wie Uuids abbilden kann. Hierfür bietet OGMConverter an, die eine Übersetzung von nicht-trivialen Attributen auf Graph-Attribute übernehmen. So dient beispielsweise der in OGM enthaltene UuidStringConverter dazu, Uuid-Objekte in der Datenbank als Strings zu speichern.

Eigene Converter können leicht als Implementierung von org.neo4j.ogm.typeconversion.AttributeConverter hinzugefügt werden, eigene IdStrategies als Implementierung von org.neo4j.ogm.id.IdStrategy. So zeigt das Beispiel in Listing 5 ein Mapping, welches eine Uuid statt der nativen Id verwendet.

Listing 5: Verwendung einer Uuid in OGM

@Id
@GeneratedValue(strategy = UuidStrategy.class)
@Convert(UuidStringConverter.class)
private UUID id;

Migration

Aufgrund der Dominanz der relationalen Datenbanken ist davon auszugehen, dass bei Einführung einer Neo4j-Datenbank im produktiven Betrieb relationale Bestandsdaten existieren, die entweder in die Neo4j übernommen werden müssen oder bestehen bleiben und durch die Neo4j nur erweitert werden. Beide Anwendungsfälle sind leicht durch die Beschaffenheit der Neo4j zu lösen.

Für die Ablösung einer relationalen Datenbank bietet Cypher Funktionen zum Import von Daten aus json, csv, xml und anderen Formaten. Auf diesem Wege lässt sich eine solche Ablösung durch einen Export der Daten in der relationalen Datenbank, gefolgt von einem Cypher-Import in die Neo4j lösen. Hierbei ist zu beachten, dass die Neo4j, wie erwähnt, Schwächen in der Schreiben-Performanz aufweist, ein solcher Groß-Import also durchaus etwas dauern kann.

Soll Neo4j ein relationales Datensystem erweitern, empfiehlt sich die Verwendung von Plugins. APOC bietet für die meisten gängigen Datenbank-Technologien Plugins mit Connectoren. Auf diesem Wege können Cypher-Statements geschrieben werden, die die Verteilung der Daten auf Neo4j und relationale Datenbank übernehmen und es so ermöglichen, beispielsweise Versionierungs-Systeme umzusetzen, deren Stammdaten in einer relationalen Datenbank gehalten werden, die Vorgänger- und Nachfolger-Relationen jedoch in der Neo4j gepflegt werden.

Fazit

Neo4j ist eine Enterprise-Ready-Datenbank, die durch die Tatsache, dass sie ACID und voll clusterfähig ist, relationalen Datenbanken in nichts nachsteht. Bedingt durch die Natur der Graph-Domäne besitzt sie eine sehr schnelle Lesegeschwindigkeit, da diese unabhängig von der Menge der Daten ist. Dies hat zur Folge, dass sie in vielen Anwendungsfällen eine Verbesserung zur relationalen Datenbank darstellt. Die Abfragesprache Cypher, die Java-basierte Plugin-Entwicklung, OGM und ein simpler Datenimport führen dazu, dass diese Datenbank-Technologie leicht zu erlernen und schnell produktiv einsetzbar ist.

Autor

Stefan Schubert

Stefan Schubert investiert seit mehreren Jahren viel Freizeit in die Vertiefung seines Wissens rund um das Thema Neo4j in Java EE und Spring.
>> Weiterlesen
botMessage_toctoc_comments_9210