Graphendatenbank: Flexible Datenabfragen mit Neo4j

Cypher

Einfache Abfragen und allgemeingültige Metriken

Die Daten sind nun modelliert und gespeichert, der interessante Teil sind jedoch deren Abfragen. Neo4j bietet hierfür die Sprache Cypher an. Diese folgt einem interessanten Ansatz, der sich kurz mit "Pattern-Matching by ASCII-Art" umschreiben lässt.

Die grundlegende Aufgabe einer Cypher-Abfrage ist es, in einer großen Menge von Knoten und Beziehungen nach Mustern zu suchen, diese nach bestimmten Kriterien zu filtern und ausgewählte Elemente aus dem Suchergebnis zurückzugeben. Die größte Herausforderung liegt dabei in der Beschreibung der zu suchenden Muster. Das soll ein Beispiel demonstrieren, das eine Liste aller Klassen und ihrer jeweiligen Superklassen ermittelt. Es handelt sich dabei um eine Suche nach allen Knotenpaaren t1 und t2, wobei

  • t1 die Labels TYPE und CLASS besitzen muss und
  • zwischen t1 und t2 eine Beziehung r vom Typ EXTENDS existiert.

Das lässt sich unter Zuhilfenahme runder Klammern für Knoten sowie eckiger Klammern für Beziehungen in ASCII-Art umsetzen:

match
(t1:TYPE:CLASS)-[r:EXTENDS]->(t2:TYPE)
return
t1.FQN as Class, t2.FQN as SuperClass
limit 20

Es handelt sich bereits um eine vollständige Cypher-Abfrage, die aus einer match-Klausel mit dem zu suchenden Muster sowie einer return-Klausel mit den zurückzugebenden Werten (t1.FQN und t2.FQN, also die voll qualifizierenden Namen der gefundenen Java-Typ-Knoten) besteht.

Bei näherem Hinschauen fällt auf, dass die Variable r für die Beziehung EXTENDS zwar deklariert, aber nicht weiter verwendet wird – sie kann komplett entfallen und spielt in den folgenden Beispielen daher keine Rolle mehr. Mit weiteren Cypher-Mitteln lässt sich das Szenario ausbauen: Welche Klassen haben die tiefsten Vererbungshierarchien?

match
h=(t1:TYPE:CLASS)-[:EXTENDS*]->(t2:TYPE)
return
t1.FQN as Class, length(h) as Depth
order by
Depth desc
limit 20
Top-10-Klassen der JRE 7 und die Tiefe ihrer Vererbungshierarchien (Abb. 2)

Die beschriebene Abfrage ist bei Kenntnissen von Cypher und des auf die Bedürfnisse eines Java-Entwicklers zugeschnittenen Datenmodells relativ einfach zu schreiben und zu lesen. Interessant ist der Vergleich zu relationalen Datenbanken: Abgesehen davon, dass in diesem Umfeld Fremdschlüsselbeziehungen die Lesbarkeit stark einschränken würden, wäre die Abfrage in der Form ohne spezielle Hilfsmittel nicht einmal formulierbar, da die Tiefe der Hierarchien unbekannt ist.

Kontextspezifische Abfragen

Die bisherigen Abfragen stellen Anwendungsfälle dar, die gängige Java-Werkzeuge problemlos abbilden können. Interessant wird es allerdings, wenn es darum geht, Informationen zu ermitteln, in die spezifische Eigenschaften eines Softwaresystems einfließen sollen. Das soll folgendes Szenario skizzieren: Eine Anwendung besteht aus einer Menge fachlicher Module und soll als einfache Komplexitätsmetrik die Anzahl der Packages pro Modul ermitteln. Dafür ist aber Wissen über die Strukturierung des Systems erforderlich, das für Werkzeuge im Normalfall nicht zugänglich ist: Welche Packages stellen die Wurzeln fachlicher Module dar? Das folgende Codebeispiel zeigt beispielhaft eine Package-Struktur, die darüber hinaus auch technische Schnitte (ui, ejb und model) enthält:

com.buschmais.shop.user
com.buschmais.shop.user.ui
com.buschmais.shop.user.ejb
com.buschmais.shop.user.model
com.buschmais.shop.cart
com.buschmais.shop.cart.ui
com.buschmais.shop.cart.ejb
com.buschmais.shop.cart.model
com.buschmais.shop.catalog
...

Die fachlichen Module liegen direkt unterhalb des Packages com.buschmais.shop und können wiederum eine beliebige Menge untergeordneter Packages enthalten. Die gewünschte Abfrage via Cypher sieht folgendermaßen aus:

match
(rootPackage:PACKAGE)-[:CONTAINS]->(modulePackage:PACKAGE),
(modulePackage)-[:CONTAINS*]->(subPackage:PACKAGE)
where
rootPackage.FQN = 'com.buschmais.shop'
return
modulePackage.FQN as Module, count(subPackage) as PackagesPerModule
order by
PackagesPerModule desc

Die Aussagekraft der Metrik ist allerdings begrenzt: Viele Packages müssen nicht zwangsläufig viele Java-Typen enthalten. Daher soll für zusätzlichen Informationsgewinn eine weitere Abfrage formuliert werden, die die Anzahl der Java-Typen pro fachlichem Modul ermöglicht. Diese würde aber die Teile der match- und where-Klauseln aus der ersten Abfrage duplizieren, die der Identifikation der fachlichen Module dienen – eine Wiederverwendbarkeit wäre wünschenswert. In solchen Fällen empfiehlt sich eine Anreicherung des Datenmodells um kontextspezifische Konzepte. Das bedeutet konkret, dass die Package-Knoten, die fachliche Module repräsentieren, zunächst mit einem Label MODULE versehen werden:

match
(rootPackage:PACKAGE)-[:CONTAINS]->(modulePackage:PACKAGE),
where
rootPackage.FQN = 'com.buschmais.shop'
set
modulePackage:MODULE
return
modulePackage.FQN as Module

Darauf basierend lässt sich die Metrik "Packages pro Modul" deutlich vereinfachen und die Metrik "Java-Typen pro Modul" entsprechend umsetzen:

match
(modulePackage:MODULE)-[:CONTAINS*]->(package:PACKAGE)
return
modulePackage.FQN as Module, count(package) as PackagesPerModule
order by
PackagesPerModule desc


match
(modulePackage:MODULE)-[:contains*]->(type:TYPE)
return
modulePackage.FQN as Module, count(type) as TypesPerModule
order by
PackagesInModule desc