Entwurf und Implementierung eines Clojure-Treibers für ArangoDB Peter Fessel Matrikel-Nr.: 772676 Medieninformatik Bachelor Beuth Hochschule für Technik Berlin Betreuer: Prof. Dr. Stefan Edlich Gutachter: Prof. Dr. Löser 0. Abstract The following thesis deals with the design and implementation of a driver for the multimodel NoSQL database ArangoDB. The driver is written in the programming language Clojure and makes use of ArangoDB's HTTP-Interface to send requests to the database. The goal for the driver is a simple and effective API design and a lightweight implementation, that adds as little overhead as possible to the database requests. Topics in this thesis include the functional JVM language Clojure, an examination of the field of NoSQL databases and the HTTP protocol and its origins in the REST architectural style. Peter Fessel, Berlin, April 2014 E-Mail: peter.fessel[at]rwth-aachen.de www.peterfessel.com www.github.com/lepetere Inhalt 0. Abstract.......................................................................................................................... 2 1. Einleitung.......................................................................................................................5 2. Fachliche u. technische Grundlagen.............................................................................. 8 2.1 Clojure.....................................................................................................................8 2.1.1 Eigenschaften von Clojure und Lisp............................................................... 8 2.1.2 Funktionale Programmierung und Clojure......................................................9 2.2 NoSQL.................................................................................................................. 12 2.2.1 Polyglot Persistence...................................................................................... 12 2.2.2 Definition...................................................................................................... 13 2.2.3 Dokumentbasierte Datenbanken................................................................... 15 2.2.4 Key/Value Datenbanken................................................................................16 2.2.5 Graphdatenbanken.........................................................................................17 2.2.6 Wide Column Stores..................................................................................... 18 2.2.7 Multimodel-Datenbanken............................................................................. 18 2.2.8 Zusammenfassung.........................................................................................19 2.3 REST und HTTP...................................................................................................20 2.3.1 REST Einführung..........................................................................................20 2.3.2 Ressourcen und Repräsentationen.................................................................21 2.3.3 Die Bestandteile von REST.......................................................................... 21 2.3.4 REST, HTTP und die HTTP-Methoden........................................................ 23 2.3.5 HTTP-Header................................................................................................ 25 2.3.6 REST und NoSQL.........................................................................................26 2.4 ArangoDB............................................................................................................. 27 2.4.1 Eigenschaften und Designziele..................................................................... 27 2.4.2 Speichereffizienz und Performance.............................................................. 28 2.4.3 Concurrency, Transaktionen, Skalierbarkeit und Replikation.......................29 2.4.4 ArangoDBs HTTP/REST-Interface...............................................................31 2.4.5 Datenbanken, Collections, Dokumente und Graphen in ArangoDB.............31 2.4.6 Querying........................................................................................................34 2.4.7 Indizierung.................................................................................................... 35 2.4.8 Die Bestandteile von ArangoDB................................................................... 35 3. Aufgabenstellung......................................................................................................... 38 4. Entwurf und Implementierung.....................................................................................39 4.1 Werkzeuge.............................................................................................................40 4.1.1 Versionsverwaltung....................................................................................... 40 4.1.2 Clojure Projektmanagement..........................................................................40 4.2 verwendete Libraries.............................................................................................41 4.2.1 clj-http........................................................................................................... 41 4.2.1 Cheshire.........................................................................................................41 4.3 Versionierung........................................................................................................ 42 4.4 Vergleich von APIs anderer Clojure Datenbanktreiber.........................................43 4.4.1 Monger für MongoDB.................................................................................. 43 4.4.2 Clutch für CouchDB..................................................................................... 47 4.4.3 Elastisch für Elasticsearch.............................................................................48 4.4.4 clj-orient für OrientDB..................................................................................49 4.4.5 Carmine für Redis......................................................................................... 51 4.4.6 Neocons für Neo4J........................................................................................ 52 4.4.7 Zusammenfassung.........................................................................................53 4.5 grundsätzliche Überlegungen................................................................................54 4.5.1 ähnliche Methoden........................................................................................ 54 4.5.2 Angabe von Verbindungsdaten......................................................................54 4.5.3 Überprüfung der Eingabedaten..................................................................... 55 4.5.4 Methodenbenennung..................................................................................... 56 4.5.5 Gliederung der Funktionalitäten in eigene Funktionsräume vs. Gliederung der ArangoDB HTTP-API......................................................................................57 4.6 Clarango API.........................................................................................................58 4.6.1 Clarango Core............................................................................................... 58 4.6.2 Document API............................................................................................... 59 4.6.3 Collection API............................................................................................... 60 4.6.4 Datenbank API.............................................................................................. 60 4.6.5 Query API......................................................................................................60 4.6.6 Graph API......................................................................................................61 4.6.7 Flexible Funktionssignaturen........................................................................ 61 4.7 Implementierungsdetails....................................................................................... 63 4.7.1 Error-Handling.............................................................................................. 63 4.7.2 Rückgabewerte.............................................................................................. 63 4.7.3 „klassische“ Datenbank-Methoden vs. „clojuresque“ Methoden.................65 4.7.4 Batch Requests.............................................................................................. 67 4.7.5 Allgemein verwendbare unterliegende Methoden.........................................67 4.8 Clarango System-Architektur............................................................................... 69 4.8.1 Clarango Namespaces................................................................................... 69 4.8.2 Diagramm......................................................................................................71 4.9 Exemplarische Untersuchung: Aufbau und Aufruf einer Clarango Methode ......72 5. Testing / Qualitätssicherung........................................................................................ 78 6. Anwendungsdemo....................................................................................................... 80 7. Fazit und Ausblick....................................................................................................... 83 8. Abbildungsverzeichnis.................................................................................................88 9. Quellenverzeichnis...................................................................................................... 89 9.1 Buchquellen.......................................................................................................... 89 9.2 Internetquellen...................................................................................................... 90 10. Anhang.......................................................................................................................96 10.1 Ausgaben des Anwendungsbeispiels aus Kapitel 6............................................96 10.2 Vollständige Clarango API Dokumentation...................................................... 104 10.2.1 Core API....................................................................................................104 10.2.2 Document API........................................................................................... 105 10.2.3 Collection API........................................................................................... 109 10.2.4 Datenbank API.......................................................................................... 113 10.2.5 Query API..................................................................................................114 10.2.6 Graph API..................................................................................................116 10.2.7 collection-ops API..................................................................................... 122 10.3 ArangoDB API Checkliste................................................................................ 124 4 1. Einleitung Mit dem Aufkommen des Web 2.0 und seinen sich schnell verändernden dynamischen Web-Anwendungen sowie großen und untereinander vernetzten Datenmengen ist eine neue Datenbank-Generation entstanden. Diese wird unter dem Label „NoSQL“ zusammengefasst. Nach Jahren der einseitigen Nutzung von relationalen Datenbanken in Softwareprojekten steht diese Bewegung für eine freie Auswahl verschiedener Datenbankmodelle. Ein Vertreter dieser neuen Gruppe von Datenbanken ist ArangoDB 1. ArangoDB wird seit 2011 von dem Unternehmen triAGENS aus Köln entwickelt und ist als Open Source Software frei verfügbar. Dort wo sich die meisten anderen Vertreter der NoSQL-Fraktion auf ein bestimmtes Datenmodell wie Dokumente oder Graphen festgelegt haben, deckt ArangoDB gleich drei verschiedene Datenmodelle ab: Dokumente, Graphen und Key/Value. Der Gedanke dahinter ist, dass sich die Datenbank fexibel an eine WebAnwendung während ihrer Entwicklung anpassen kann. Wenn sich in der Entwicklungsphase neue Anforderungen ergeben, so ist es nicht notwendig, gleich das ganze Datenbanksystem zu wechseln oder ein zusätzliches System zum Technologiestack hinzuzufügen. Stattdessen vereint ArangoDB viele Funktionen unter einem Dach, sodass bei Bedarf zusätzliche oder andere Funktionen genutzt werden können. Um diese vielseitigen Anwendungsmöglichkeiten zu erreichen, werden leichte Abstriche bei der Performance und bei der Skalierbarkeit gemacht. Mit diesem breiten Ansatz hat sich das Team von ArangoDB zum Ziel gesetzt, „das MySQL in NoSQL“ zu werden2. ArangoDB soll also zur quasi-Standard-Datenbank unter den NoSQL-Datenbanken werden, so wie es MySQL faktisch im Bereich der relationalen Datenbanken ist. Da die Datenbank sich in stetiger Weiterentwicklung befindet, ist es wahrscheinlich, dass sie in Zukunft zu einer breiteren Verwendung gelangt. Die Nähe von ArangoDB zum Web wird deutlich durch die Verwendung einer REST/HTTP-Schnittstelle zur Kommunikation mit seinen Clients. Ebendiese Schnittstelle soll zur Entwicklung des Clojure Treibers in dieser Bachelorarbeit genutzt werden. Der Treiber dient dazu, die Sprache Clojure mit der Datenbank ArangoDB kommunizieren zu lassen, sodass Daten ausgetauscht werden können. 1 http://www.arangodb.org/ 2 „we want to become the MySql in nosql – without MySql’s annoyances of course ;-)“ [wwwArangoBlog1] 5 Bei Clojure handelt es sich um eine vornehmlich funktionale Programmiersprache. Der Ansatz der funktionalen Programmierung versucht mit der Komplexität von Softwareanwendungen umzugehen, indem Zustände und veränderliche Variablen aus der Programmierung verbannt werden. Da es sich bei REST/HTTP ebenfalls um ein zustandsloses Konzept handelt, bietet sich eine Kombination dieser beiden Ansätze an. Als die Idee zu dieser Arbeit entstand, gab es noch keinen ArangoDB Treiber für Clojure3. Das Ziel dieser Arbeit ist daher die Entwicklung eines solchen Treibers. Der Treiber soll in Zusammenarbeit mit dem Betreuer Prof. Dr. Stefan Edlich entstehen. Prof. Dr. Edlich wird sich hierbei vornehmlich auf das Erstellen einer Test-Infrastruktur konzentrieren, während der Verfasser dieser Arbeit den Entwurf, und soweit es geht, auch die Implementierung des eigentlichen Treibers übernimmt. Der Treiber wird als Open Source Projekt unter dem Namen „Clarango“ realisiert. „Clarango“ setzt sich aus den Anfangsbuchstaben der Namen Clojure und ArangoDB zusammen. Mit Hilfe einer HTTP-Library ist es theoretisch möglich, direkt HTTP Anfragen aus einer Anwendung an die Datenbank zu senden. In der Praxis ist dies jedoch keine gute Lösung, da es die Komplexität der Anwendung unnötig erhöht. Die Verwendung eines Clojure Treibers bietet den Vorteil, dass das Senden der HTTP-Anfragen vollständig ausgelagert wird. Der Treiber übernimmt dann die Aufgabe des Zusammensetzens der HTTP-Anfragen im von der Datenbank erwarteten Format. Durch die Möglichkeit der Java-Interoperabilität bei Clojure bietet sich zwar die Verwendung des bereits verfügbaren Java Treibers für ArangoDB 4 in direkter Weise oder mittels eines Clojure-Wrappers an. Dies ist allerdings umständlich, denn unter anderem müssen bei der Verwendung des Java Treibers Objekte erzeugt werden, um mit der Datenbank zu arbeiten (z.B. Instanzen des Treibers selbst). In der funktionalen und zustandsarmen Clojure-Programmierung ist dies jedoch nicht erwünscht und Zustände sollten so weit es geht vermieden werden. Ein nativer Clojure Treiber lässt sich weitaus besser in die Clojure-übliche Programmierweise integrieren. Für die Verwendung von Clojure spricht außerdem, dass die Programmiersprache nativ die Datenstruktur der Maps unterstützt. Diese ist dem JSON-Format, das von ArangoDB verwendet wird, ähnlich und kann leicht in dieses übersetzt werden. Im Laufe dieser Arbeit wird zunächst eine Einführung in die verwendeten Technologien und zugehörigen Themengebiete gegeben. Hierbei wird ein besonderer Schwerpunkt auf 3 Eine offizielle Liste der verfügbaren Treiber kann hier eingesehen werden: https://www.arangodb.org/drivers 4 https://github.com/tamtam180/arangodb-java-driver 6 das Thema NoSQL gelegt. Anschließend wird die Datenbank ArangoDB in Bezug zu bereits existierenden Datenbanken gestellt und in das Feld der NoSQL-Datenbanken eingeordnet. Nach einer genaueren Beschreibung der Aufgabenstellung werden die Befehlssätze einiger anderer Treiber für NoSQL-Datenbanken untersucht, um eine Entscheidungsgrundlage für ein möglichst gutes Design der Clarango API 5 zu erhalten. Anschließend wird dann auf grundsätzliche Designüberlegungen in Kombination mit konkreten Implementierungsdetails eingegangen und das Design der Clarango API sowie die Architektur der Anwendung erläutert. Zum Schluss werden die zur Qualitätssicherung verwendeten Maßnahmen beschrieben und einige Codebeispiele zur möglichen Verwendung von Clarango aufgeführt. 5 API steht für „Application Programming Interface“ und bezeichnet eine Schnittstelle eines Programms, die von anderen Programmen benutzt wird um auf Dienste des Programmes zugreifen zu können. Siehe hierzu auch http://de.wikipedia.org/wiki/Programmierschnittstelle. 7 2. Fachliche u. technische Grundlagen Die folgenden Abschnitte sollen eine Einleitung bieten in für diese Arbeit verwendete und grundlegende Technologien. 2.1 Clojure Bei der Programmiersprache Clojure handelt es sich um einen Dialekt der Programmiersprache Lisp6 und um eine Sprache, die den Einsatz funktionaler Programmiermodelle fördert. Clojure wird auf der Java Virtual Machine (JVM) 7 ausgeführt, der Laufzeitumgebung in der auch die Programmiersprache Java ausgeführt wird. Clojure Code wird nach JVM-Bytecode kompiliert, bietet jedoch alle Features auch zur Laufzeit an und bleibt damit vollständig dynamisch. Clojure wurde von Rich Hickey als „general-purpose language“ geschaffen [wwwClojure] und erstmals im Jahr 2007 veröffentlicht. Ohne dass besondere Mittel des Marketings eingesetzt wurden, hatte die Sprache schnell einen größeren Kreis von Anhängern und eine lebendige Community8. 2.1.1 Eigenschaften von Clojure und Lisp Clojure ist eine dynamisch und stark typisierte Sprache. Es handelt sich um einen Dialekt von Lisp, einer der ersten Programmiersprachen überhaupt, die auch heute noch Verwendung findet. Clojure gehört dabei genau wie der heute noch verwendete Dialekt Scheme zur Familie der „lisp-1“ Dialekte [Tate2011]. Durch den Verzicht auf die Abwärtskompatibilität schlägt die Sprache jedoch, verglichen mit anderen Dialekten, eine neue Richtung ein. Diese äußert sich unter anderem in der Erweiterung der verfügbaren Datenstrukturen um Vektoren und Maps und der dazugehörigen Einführung von zusätzlichen Klammer-Typen, die die Lesbarkeit erhöhen sollen, sowie der Einführung der standardmäßigen Unveränderlichkeit der Datenstrukturen [wwwClojureRatio]. Clojure versteht sich laut Rich Hickey auch als ein „praktisches“ Lisp, denn die bisherigen LispDialekte wurden hauptsächlich in der Forschung verwendet und sind nie richtig in der produktiven Programmierung der Industrie angekommen [Tate2011]. Lisp steht für „LISt Processing“ und der Name rührt daher, dass der gesamte Code aus Listen besteht. Funktionsaufrufe verwenden das erste Element einer Liste als 6 http://de.wikipedia.org/wiki/Lisp 7 http://de.wikipedia.org/wiki/Java_Virtual_Machine 8 Vgl. „Interview with Rich Hickey“: https://www.ugtastic.com/rich-hickey/ 8 Funktionsnamen und die restlichen Elemente als deren Argumente. Da Lisp seine eigenen Datenstrukturen verwendet um Programme auszudrücken, lautet eine wichtige Strategie der Sprache „Daten als Code“ (code-as-data). Durch dieses Konzept ist die Sprache auch insbesondere gut zur Metaprogrammierung geeignet. Letztere stellt auch eine der Stärken von Clojure dar. Wie bereits erwähnt wird Clojure auf der Java Virtual Machine ausgeführt. Dies verschafft der Sprache einen infrastrukturiellen Vorteil einerseits durch die Verfügbarkeit der Plattform auf vielen (Betriebs-)Systemen und der bereits vorhandenen breit gestreuten Akzeptanz dieser Plattform; andererseits durch die Möglichkeit der Einbindung der vielen bereits existierenden Java-Bibliotheken (Java-Interoperabilität). Die Vielseitigkeit von Clojure wird noch vergrößert durch einen existierenden Clojure-nach-JavaScript Compiler namens ClojureScript9, der die Ausführung in JavaScript-Umgebungen erlaubt. 2.1.2 Funktionale Programmierung und Clojure Bei Clojure handelt es sich um eine Programmiersprache, die teilweise auch imperative Programmierung zulässt [Tate2011]. In erster Linie ist Clojure jedoch eine funktionale Programmiersprache und die funktionale Programmierung wird von Clojure stark gefördert. Daher soll dieses Programmierparadigma und damit weitere wichtige Eigenschaften der Sprache Clojure hier kurz erläutert werden. Eine sehr gute Definition der funktionalen Programmierung findet sich auf S.13 in [Edlich2011]: „Das Konzept einer Funktion im Sinne der Mathematik ist in der funktionalen Programmierung am klarsten umgesetzt. Hier stellen die Funktionen Abbildungsvorschriften dar. Eine Funktion besteht dann aus einer Reihe von Definitionen, die diese Vorschrift beschreibt. Ein funktionales Programm besteht ausschließlich aus Funktionsdefinitionen und besitzt keine Kontrollstrukturen wie Schleifen. Wichtigstes Hilfsmittel für die funktionale Programmierung ist daher die Rekursion. Funktionen sind in funktionalen Programmiersprachen Objekte, mit denen wie mit Variablen gearbeitet werden kann. Insbesondere können Funktionen als Argument oder Rückgabewert einer anderen Funktion auftreten. Man spricht dann von Funktionen höherer Ordnung.“ Folgende Eigenschaften und Ideen zeichnen sowohl die funktionale Programmierung als auch die Programmiersprache Clojure aus: 9 https://github.com/clojure/clojurescript 9 • First-Class Functions: Damit bezeichnet man die Tatsache, dass Funktionen selber Werte sind. Mit ihnen kann genau wie mit anderen Daten gearbeitet werden. Das heißt insbesondere können sie anderen Funktionen als Argumente übergeben werden und von diesen als Ergebnisse zurückgegeben werden. • Funktionen höherer Ordnung: Die Existenz von Funktionen höherer Ordnung resultiert direkt aus den First-Class Functions: Es handelt sich hierbei um Funktionen, die andere Funktionen als Argumente übergeben bekommen oder Funktionen als Ergebnisse zurückliefern. Klassische Beispiele aus der Welt der funktionalen Programmierung sind die Funktionen Map und Reduce, die jeweils eine Funktion übergeben bekommen, die sie auf allen Elementen einer Collection ausführen. • unveränderliche Werte (immutable Values): Die zentralen Datenstrukturen in Clojure sind unveränderlich, anders als zum Beispiel Variablen in Sprachen wie Java oder JavaScript. Dadurch wird die Fehleranfälligkeit von Programmen reduziert, denn es werden die sogenannten „Pure Functions“ (siehe unten) erst möglich. Außerdem ist das Arbeiten mit unveränderlichen Werten ein wichtiger Aspekt der nebenläufigen Programmierung, einer der Stärken von Clojure: wird mit mehreren Threads parallel auf denselben Datenstrukturen gearbeitet, können diese sich darauf verlassen, dass die Daten nicht gleichzeitig von anderen Threads verändert werden. Dies erleichtert die nebenläufige Programmierung stark. • keine Seitenefekte: Als Seiteneffekte werden Interaktionen einer Funktion mit der „Außenwelt“ bezeichnet; per Definition handelt es sich bei jedem Input/Output eines Programms oder der Modifikation eines veränderlichen Objekts um einen Seiteneffekt [Emerick2012]. • Pure Functions: „Pure Funktionen“ verzichten auf die Nutzung von Seiteneffekten. Bei gleichen Eingabewerten resultieren hier immer die gleichen Ausgabewerte. Dadurch ist das Verhalten einer Funktion zu hundert Prozent vorhersagbar, was sie wiederum sehr gut testbar macht und die Fehleranfälligkeit eines Programms reduziert. Die rein funktionale Programmierung kann man sich auch wie einen Baum vorstellen: Es wird eine Funktion aufgerufen, die die Wurzel des Baumes bildet und wiederum weitere Funktionen aufruft, welche wiederum weitere Funktionen aufrufen usw. Am Ende eines jeden Astes liefert jeweils die unterste Funktion einen Rückgabewert, der dann an die aufrufenden Funktionen weitergereicht bzw. von diesen weiterverarbeitet wird und ebenfalls an die aufrufende Funktion zurückgegeben wird bis wieder die Wurzel des Baumes erreicht ist. 10 Da in Programmen jedoch immer irgendeine Art von Eingabe und Ausgabe erfolgen muss, damit das Programm einen sinnvollen Zweck erfüllen kann, sind rein funktionale Programme in der Realität nicht möglich. Es wird stattdessen vielmehr auf eine Mischform zurückgegriffen, wie sie in Abbildung 1 dargestellt ist. Ein Programm kann einen funktionalen Kern haben, in dem alle Vorteile der funktionalen Programmierung genutzt werden können. Es muss jedoch auch über einen Teil verfügen, der mit der restlichen Welt kommunizieren kann und deswegen nicht seiteneffektfrei ist. Abb. 1: Diagramm eines funktionalen Programms 11 2.2 NoSQL Bei NoSQL handelt es sich um eine ca. seit dem Jahr 200410 stattfindende Bewegung weg von den allseits verbreiteten relationalen Datenbanken hin zu alternativen Datenbankmodellen. Insbesondere hat sich der Bedarf nach neuen Datenbanktypen durch die rasante Entwicklung des World Wide Web und besonders des sogenannten „Web 2.0“ ergeben. Einerseits da im Web 2.0 mit besonders großen Datenmengen (Big Data) gearbeitet wird, wie diese zum Beispiel in sozialen Netzwerken vorkommen, und diese effektiv und skalierbar gespeichert und verarbeitet werden sollen. Andererseits, da die neuen, meist schemalosen Datenbanken eine agile Entwicklung, wie sie häufig bei WebStartups gefordert wird, unterstützen. 2.2.1 Polyglot Persistence Ein bekanntes Stichwort der Bewegung lautet „Polyglot Persistence“. Damit gemeint ist, dass unterschiedliche Anforderungen in Anwendungen auch mit unterschiedlichen Datenbanken gelöst werden sollten. Unterschiedliche Datenbanktypen wurden für bestimmte Zwecke geschaffen. Zu versuchen, alle unterschiedlichen Problembereiche mit nur einem Datenbanktypen zu lösen, das heißt unterschiedliche Datentypen in das selbe Schema zu pressen, ist oft kontraproduktiv und mündet in schlechter Performanz. In [Sadalage2013] findet sich dazu ein Beispiel anhand einer E-Commerce Plattform: Es macht wenig Sinn das Session-Management, den Benutzer-Einkaufswagen, die Bestell- und Produktdaten sowie Kaufempfehlungen einer solchen Anwendung im selben Datenbankmodell abzulegen. Denn es bestehen hier unterschiedliche Anforderungen an die Konsistenz, die Verfügbarkeit, die Skalierbarkeit und die Datensicherheit. Die Autoren schlagen hier einen hybriden Ansatz vor, in dem für das Session-Management und den Einkaufswagen ein Key/Value-Store benutzt wird. Dieser ist schnell und skalierbar und eignet sich hier insbesondere, da der Datenzugriff üblicherweise über bekannte User- und Session-IDs erfolgen wird. Ein erhöhtes Maß an Datensicherheit ist dabei nicht erforderlich. Abgeschickte Bestellungen und das Produkt-Inventar können dagegen in einer traditionellen relationalen Datenbank abgespeichert werden. Alternativ könnte aber auch eine dokumentbasierte Datenbank genutzt werden. Für Kaufempfehlungen aufgrund von bereits gekauften Produkten oder den Bestellungen anderer Kunden eignet sich dagegen insbesondere eine Graph-Datenbank, da diese in besonderem Maße dafür geeignet ist, Verknüpfungen von Datensätzen untereinander abzubilden. 10 2004: Entwicklung von Googles BigTable und GFS (Google File System), aufgrund derer laut [Edlich2011] Google als der „NoSQL-Vorreiter schlechthin“ gilt 12 Die Autoren von [Edlich2011] sehen zudem hinter dem Begriff Polyglot Persistence eine Art Bewegung für eine freie Datenbank-Auswahl. Oft sind Unternehmen durch Verträge oder auch durch mangelndes Wissen auf genau eine Datenbank festgelegt und so müssen die unterschiedlichsten Datenstrukturen in unzählige relationale Datenbanken „gepresst“ werden. Die Autoren fordern, dass vor der Auswahl einer Datenbank die Anforderungen genauer untersucht werden und auch schon in der Lehre mehr darauf geachtet wird, Kenntnisse über die unterschiedlichen verfügbaren (NoSQL-)Systeme zu vermitteln. 2.2.2 Definition Der Begrif NoSQL war zu Beginn der Bewegung als eine Art Negativ-Definition zu verstehen, der auf eine Abkehr der auf der Abfragesprache SQL basierenden relationalen Datenbanken hinwies. Mittlerweile wird der Begriff von der Community aber auch als „Not only SQL“ definiert. Dies ist wohl darauf zurückzuführen, dass auch einige NoSQL Datenbanken eine Abfrage mittels SQL oder einer (evtl. erweiterten) Untermenge dieser Sprache ermöglichen. Eine scharfe Trennung der Bereiche der klassischen relationalen, SQL-basierten Lösungen und den Datenbanken der NoSQL-Fraktion wird erschwert durch eine Vielzahl an Hybridlösungen zwischen beiden Welten und durch viele unterschiedliche Meinungen darüber, wo eine mögliche Grenze zu ziehen ist [Edlich2011]. Die Autoren von [Edlich2011] haben aber einen Versuch gewagt und 7 Kriterien entwickelt, um NoSQL Datenbanken als solche zu identifizieren11: 1. „Das zugrunde liegende Datenmodell ist nicht relational.“ → hier hinter verbirgt sich die Erkenntnis, dass das relationale Datenmodell nicht immer das passendste für ein Problem sein muss; 2. „Die Systeme sind von Anbeginn an auf eine verteilte und horizontale Skalierbarkeit ausgerichtet.“ → die herkömmlichen relationalen Datenbanken waren immer schwerer für große Web 2.0 Anwendungen zu skalieren; die Datenbanken der NoSQL-Bewegung haben dieses Problem von Anfang an mit im Design berücksichtigt; hier können durch horizontale Skalierung auch auf Standard-Hardware sehr große Datenmengen efektiv verwaltet werden; 11 Es müssen jedoch nicht alle Punkte zwingend erfüllt sein, um eine Datenbank zur NoSQL-Fraktion zu zählen. 13 3. „Das NoSQL-System ist Open-Source.“ → dies ist das wohl am wenigsten strikt gemeinte Kriterium; ist ein System nicht Open Source, ist das kein Ausschlusskriterium, aber viele der NoSQL-Systeme sind frei verfügbar und verstehen sich als eine Art Protestbewegung gegen die Dominanz der (teilweise kostspieligen) relationalen Systeme; 4. „Das System ist schemafrei oder hat nur schwächere Schemarestriktionen.“ → hierdurch ergibt sich die bereits erwähnte Möglichkeit der agilen Entwicklung und fexiblen Änderung und Erweiterung, wie sie mit konventionellen relationalen Datenbanken und ihrem starren Tabellensystem schwer umzusetzen ist; 5. „Aufgrund der verteilten Architektur unterstützt das System eine einfache Datenreplikation.“ → dies wurde ebenfalls bei den meisten Systemen von Anfang an als Anforderung mit umgesetzt; oft kann durch ein einziges Kommando eine ganze Datenbank repliziert werden und auf einem zusätzlichen Server-Knoten bereitgestellt werden; 6. „Das System bietet eine einfache API.“ → Datenbank-Anfragen konventioneller SQL-Systeme können durch viele JoinOperationen leicht kompliziert werden; außerdem ergibt sich eine gewisse Fehleranfälligkeit und Starrheit dadurch, dass die Anfragen in Form von Strings formuliert werden; NoSQLLösungen bieten hier häufg eine einfachere API; Beispiel sind etwa Datenbanken, deren Interaktion komplett über eine REST-Schnittstelle läuft12; bei komplexen DatenbankAnfragen hat jedoch meist noch SQL die Nase vorn, denn in NoSQL-Systemen müssen diese häufg als Map/Reduce-Abfragen formuliert werden; 7. „Dem System liegt meistens auch ein anderes Konsistenzmodell zugrunde: Eventually Consistent und BASE, aber nicht ACID.“ → bei Web 2.0 Anwendungen (wie zum Beispiel bei Social Media Portalen) handelt es sich häufg um nicht sicherheitskritische Anwendungen (im Gegensatz zum Beispiel zu einer Bankanwendung); es muss damit häufg kein klassisches ACID-System 13 verwendet werden, sondern die Daten können auch für einen kurzen Zeitraum inkonsistent sein, was der Skalierbarkeit und der Verfügbarkeit zugute kommt; es reicht, wenn die Daten „eventually consistent“ sind14; 12 Siehe hierzu auch Abschnitt 2.3.6 13 ACID steht für Atomicity, Consistency, Isolation, Durability und beschreibt grundsätzliche Eigenschaften von Verarbeitungsschritten in Datenbank-Systemen; siehe hierzu auch http://de.wikipedia.org/wiki/ACID 14 Siehe hierzu auch http://en.wikipedia.org/wiki/Eventual_consistency; das dazugehörige Konsistenzmodell wird auch als BASE für Basically Available, Soft state, Eventual consistency bezeichnet; 14 2.2.3 Dokumentbasierte Datenbanken Eine dokumentbasierte Datenbank speichert Dokumente. Bei diesen Dokumenten handelt es sich aber im Gegensatz zu „echten“ Dokumenten wie zum Beispiel Textdateien 15 um strukturierte Datensammlungen wie Hashes oder JSON 16. Es gibt in den Dokumenten IDFelder und dazugehörige Values (Werte), die wiederum weiteren Dokumenten entsprechen können. So können Daten beliebig geschachtelt werden. Dadurch dass die meisten Datenbanken auf diesem Gebiet schemafrei sind, ist dieses Datenmodell sehr fexibel. Die wohl bekanntesten Vertreter sind hier MongoDB17 und CouchDB18. Bei beiden handelt es sich um Open Source Projekte. MongoDB wurde erstmals im Jahr 2009 veröffentlicht und CouchDB bereits 2005. Beide Datenbanken haben viele Gemeinsamkeiten. So eignen sich beide sowohl für kleine als auch für sehr große Anwendungen [Redmond2012]. Bei beiden Datenbanken werden die Daten als JSON-Dokumente abgespeichert19, es wird JavaScript als primäre Interaktionssprache eingesetzt und beide bieten Map/Reduce-Funktionen20. Bei CouchDB funktioniert die Abfrage per sogenannter Views, in denen Map/Reduce-Funktionen spezifiziert sind; die Views speichern die Ergebnisse der Abfrage zwischen, bis sich die beteiligten Daten ändern. MongoDB bietet zusätzlich zu Map/Reduce noch Ad Hoc Querying 21 und den Zugriff per Indices, wie man ihn aus relationalen Systemen gewohnt ist. So wird eine Brücke geschlagen zwischen klassischen relationalen Systemen und den Vorteilen der schemafreien, verteilten NoSQL Systeme. CouchDB bezeichnet sich selber als „Database for the Web“ und ist sehr nah an WebTechnologien gebaut. Als Interface zur Interaktion mit der Datenbank dient HTTP/REST. CouchDB bietet im Gegensatz zu MongoDB ein integriertes Browser-Interface, welches das Durchsuchen und Anlegen von Datensätzen erlaubt 22. Und mit CouchApps lassen sich sogar Webseiten und (über JSON-Dokumente hinausgehende) Inhalte direkt an den Browser senden, ohne eine weitere Softwareschicht dazwischen23. 15 Der Begriff der Dokumentendatenbank stammt von der Datenbank Lotus Notes, wo noch echte Anwenderdokumente in der Datenbank gespeichert wurden [Edlich2011] 16 http://de.wikipedia.org/wiki/JSON 17 https://www.mongodb.org/ 18 http://couchdb.apache.org/ 19 Bei MongoDB genauer gesagt als BSON, was für „Binary JSON“ steht (siehe http://bsonspec.org/). 20 http://en.wikipedia.org/wiki/Map_reduce 21 Als Ad Hoc Queries bezeichnet man Queries, deren Inhalt erst zum Zeitpunkt der Ausführung in der Anwendung bekannt ist; im Gegensatz zu vordefinierten Queries wie „zeige alle Datensätze der Datenbank“; vgl. dazu auch http://www.learn.geekinterview.com/data-warehouse/dw-basics/what-isan-ad-hoc-query.html 22 Bei MongoDB sind hier aber Lösungen von Drittanbietern verfügbar; vgl. http://docs.mongodb.org/ecosystem/tools/administration-interfaces/ 23 Vgl. http://couchapp.org/page/what-is-couchapp 15 CouchDBs Design wurde auf hohe Verfügbarkeit und Datensicherheit ausgerichtet. Zu jedem Dokument wird nicht nur eine ID abgespeichert, sondern auch eine RevisionsNummer für jeden Änderungszustand des Dokuments seit seiner Entstehung. Jeder Stand des Dokuments wird mit der dazu gehörigen Revisions-Nummer abgespeichert und ist zur Abfrage verfügbar. Änderungen an Dokumenten werden nur durchgeführt, wenn der Benutzer zusätzlich zur Dokumenten-ID auch noch die Revisions-Nummer seiner aktuellsten Version kennt. Dieses append-only Storage Modell macht die Daten sehr sicher und ermöglicht eine leichte Replikation und Wiederherstellung, auch wenn Teile des Netzwerks ausfallen sollten. Ein Nachteil ist, dass die Datenbank-Größe schnell zunimmt, wenn sich die Daten häufig ändern. MongoDBs Design wurde stark auf horizontale Skalierbarkeit ausgelegt. Wo bei CouchDB vor allem vertikale Skalierung durch die Replikation und Bereitstellung der Daten auf verschiedenen Servern möglich ist, ermöglicht MongoDB zusätzlich die horizontale Skalierung durch das sogenannte Sharding. Hierbei werden Collections in Teile aufgeteilt, die dann auf verschiedenen Servern bereitgestellt werden [Redmond2012]. Als weiterer Vertreter in der Gattung dokumentbasierter Datenbanken soll hier noch Riak erwähnt werden. In dieser Datenbank werden üblicherweise Dokumente gespeichert, diese jedoch per Key/Value-Funktionalität in sogenannten Bucket-Namensräumen abgespeichert und in einem Ring-Adressraum verwaltet, weshalb die Entwickler bei Riak von einer Key/Value-Datenbank sprechen [Edlich2011]. Riak wird deswegen im nächsten Abschnitt nochmals beschrieben. 2.2.4 Key/Value Datenbanken Bei Key/Value handelt es sich um ein sehr einfaches Datenbankmodell. Hier werden Keys mit Values, also jeweils einem zum Key gehörigen Wert, gepaart. Key/Value Datenbanken sind aufgrund dieser einfachen Datenstruktur ohne Relationen leicht skalierbar. Welcher Datentyp dabei als Value gespeichert werden kann, variiert dabei von Datenbank zu Datenbank. Bekannte Vertreter der Key/Value Gattung sind Redis 24 sowie das bereits erwähnte Riak25, beides Open Source-Projekte. Redis gilt als sehr schneller Key/Value-Store, da hier alle Daten im RAM gespeichert werden und nur von Zeit zu Zeit mit der Festplatte synchronisiert werden. Es werden die unterschiedlichsten Datentypen angeboten, die als Werte gespeichert werden können: Strings, Hashes, Listen, Sets und sortierte Sets. Des Weiteren werden atomische 24 http://redis.io/ 25 http://basho.com/riak/ 16 Operationen auf den Datenstrukturen angeboten sowie Message Queues mit publish/ subscribe-Funktionalität. Bei Riak handelt es sich um eine sehr vielseitige Datenbank. Sie wurde mit den Zielen Verfügbarkeit, Fehlertoleranz und Skalierbarkeit entworfen. Die eigentliche Speicherengine ist hier austauschbar. Grundsätzlich werden in Riak Dokumente gespeichert. Diese werden in sogenannten Bucket-Namensräumen verwaltet, in denen dann die Keys abgelegt werden. Durch die Möglichkeit, Links zwischen den Dokumenten abzuspeichern, können mit Riak auch Graphen- oder relationale Strukturen umgesetzt werden. Als Konsistenzmodell wird hier BASE/Eventually Consistent angewendet und Riak bietet Map/Reduce. Wie bei CouchDB erfolgt der Zugriff auf die Datenbank immer über REST/HTTP-Anfragen. Einer der Unterschiede zwischen den beiden Systemen ist jedoch, dass Riak mehr auf die Skalierung und Verteilung der Daten ausgelegt ist [Edlich2011]. 2.2.5 Graphdatenbanken Graphdatenbanken speichern untereinander vernetzte Strukturen, die aus Knoten und ihren Verbindungen, den Kanten, bestehen. Zeichnen sich Datensätze durch eine große Anzahl an Verlinkungen der Einheiten untereinander aus und müssen in der Anwendung diese Datensätze oft anhand ihrer Verlinkungen durchlaufen (traversiert) werden, so ist meist eine Graph-Datenbank eine gute Wahl. Im Web-Umfeld werden diese zum Beispiel oft im Bereich des Social Networking verwendet. Der wahrscheinlich bekannteste Vertreter der Fraktion der Graphdatenbanken ist Neo4j26. Es handelt sich bei Neo4J um eine hochskalierbare und gleichzeitig leichtgewichtige Open Source Datenbank. Als Datenmodell bietet Neo4j Knoten sowie gewichtete Kanten, wobei beide Typen Eigenschaften in Form von beliebigen Daten annehmen können 27. Die Datenbank bietet ACID-Konsistenz und eine eigene Query-Language namens Cypher. Der Zugriff auf die Daten kann abgesehen von Cypher entweder per REST-Interface oder einer objektorientierten Java-Schnittstelle erfolgen. Ein Browser-Interface mit einer komfortablen Graph-Visualisierung ist ebenfalls bereits in die Standard-Version integriert. Des Weiteren soll kurz erwähnt werden, dass es mit dem Tinkerpop Blueprints Projekt eine standardisierte Open Source API für die Programmiersprache Java gibt, welche einen einheitlichen Zugriff auf Graph-Datenbanken ermöglicht und die von vielen Graphdatenbanken, darunter auch Neo4J 28, implementiert wird. Neo4J bietet damit zwei 26 http://www.neo4j.org/ 27 Das sich hier hinter verbergende Modell wird auch „Property Graph“ genannt. 28 https://github.com/tinkerpop/blueprints/wiki/Neo4j-Implementation 17 Query-Languages, das bereits erwähnte Cypher, sowie die Abfragesprache Gremlin, die Teil von Tinkerpop Blueprints ist. Cypher weist dabei eher Ähnlichkeiten mit SQL auf, während Gremlin mit seinem Collection-orientierten Zugriff Ähnlichkeiten zum DOMZugriff in jQuery aufweist [Redmond2012]. 2.2.6 Wide Column Stores Eine weitere Gruppe im NoSQL-Bereich sind die Wide Column Stores. Diese ähneln den relationalen Datenbanken. Die Daten werden hier ebenfalls in Tabellen gespeichert, jedoch anders als in relationalen Datenbanken ist die Speicherung nicht zeilen- sondern spaltenorientiert. Das bedeutet, dass physisch auf dem Speichermedium nicht die Datensätze (oder Tupel) hintereinander gespeichert werden, sondern die Attribute einer Spalte. Dies bietet Vorteile bei der Analyse der Daten, bei der Datenkompression [Edlich2011] und lässt außerdem zu, dass kein Speicherplatz verschwendet wird, sollten nicht alle Spalten einer Datenbankzeile mit Werten belegt sein. Weiterhin ist das Hinzufügen von Spalten wesentlich zeitefzienter als bei zeilenorientierten Datenbanken. Es gibt aber auch Nachteile. Hierzu zählen der größere Aufwand beim Suchen und Einfügen von Daten sowie beim Lesen von zusammengehörigen Datensätzen. Die drei bekanntesten Vertreter der Gattung der spaltenorientierten Datenbanken sind HBase, Cassandra und Hypertable (hier stimmen [Redmond2012] und [Edlich2011] überein). Diese Datenbanken orientieren sich allesamt an Googles BigTable 29, weichen jedoch von der oben beschriebenen Idee etwas ab und bieten eine Art Kombination aus spaltenorientiertem Design in Verbindung mit Key/Value-Funktionalitäten. Durch den Einsatz von mehrdimensionalen Tabellen in Kombination mit einer guten Skalierbarkeit eignen sich diese Datenbanken sehr gut für besonders große Datenmengen. Wide Column Stores spielen für diese Bachelorarbeit keine besondere Rolle und wurden deshalb nur der Vollständigkeit halber erwähnt. 2.2.7 Multimodel-Datenbanken Multimodel-Datenbanken vereinen die Konzepte vieler NoSQL-Datenbanken in einem System. Neben der Datenbank ArangoDB, die später näher untersucht wird, ist OrientDB30 ein Vertreter dieser Gattung. Bei OrientDB handelt es sich um eine Open Source Datenbank, deren Basis eine Dokument-Datenbank ist. Diese wurde jedoch um Graphen-Funktionalitäten erweitert und so lassen sich genau wie bei Riak in den Dokumenten auch Links zu anderen Dokumenten abspeichern. OrientDB unterstützt 29 http://de.wikipedia.org/wiki/BigTable 30 http://www.orientdb.org/ 18 Tinkerpop Blueprints und somit ist auch die Graphen-Traversierung mittels Gremlin möglich. Der allgemeine Zugriff ist mittels einer erweiterten Untermenge von SQL als Abfragesprache, über die native Java-API, sowie über eine REST/HTTP-Schnittstelle möglich. Weiterhin verfügt OrientDB über ein umfangreiches Rechtemanagement für Benutzer, es werden ACID-Transaktionen unterstützt und Dokumente können sowohl mit als auch ohne Schemata benutzt werden sowie zusätzlich in einem gemischten Modus. Zum Abschluss soll hier erwähnt werden, dass auch die Datenbank Riak über Merkmale einer Multimodel-Datenbank verfügt. Genau wie in OrientDB werden bei Riak die Daten in Dokumenten gespeichert und diese können zusätzlich über Verlinkungen verfügen. Riak unterstützt zwar nicht wie OrientDB die Tinkerpop Graph-API zur Traversierung, es lassen sich aber sehr wohl Graphen-Strukturen hiermit abbilden. OrientDB kann außerdem ebenso wie Riak, das in erster Linie als Key/Value-Datenbank gilt, als Key/ValueDatenbank verwendet werden31. 2.2.8 Zusammenfassung Man sieht, dass die Grenzen bei den Datenmodellen der NoSQL-Datenbanken teilweise fießend sind und es einige Mischformen gibt, bzw. Aspekte von NoSQL-„Genres“ in andere übernommen werden. Je nach Anwendungsbereich ist ein bestimmtes Datenmodell besonders passend. Bei komplexen Anwendungen mit unterschiedlichen Anforderungen bietet sich die Verwendung mehrerer unterschiedlicher Datenbanktypen nach dem Konzept der „Polyglot Persistence“ an. In anderen Anwendungsbereichen macht dagegen eher die Verwendung einer Multimodel-Datenbank Sinn, die die Konzepte mehrerer Typen vereint. Zu diesem Typ zählt auch die Datenbank ArangoDB, die Gegenstand dieser Arbeit ist. ArangoDB wird in Abschnitt 2.4 näher erläutert. In Abschnitt 4.4 werden außerdem einige Clojure Treiber für die bisher vorgestellten NoSQL-Datenbanken verglichen und untersucht. 31 Vgl. https://github.com/orientechnologies/orientdb/wiki/Key-Value-engine 19 2.3 REST und HTTP 2.3.1 REST Einführung REST steht für REpresentational State Transfer und ist ein Entwurfsmuster, welches ein verteiltes System bestehend aus Client und Server extrem skalierbar macht. Als grundlegende Architektur des Web machte REST dessen enormes Wachstum und dessen enormen Erfolg erst möglich. Dennoch bietet REST einen hohen Grad von Anpassbarkeit und lässt Kompromisse zu [Tilkov2009]. Das REST-Prinzip wurde von Roy Thomas Fielding, der vorher bereits das Protokoll HTTP mitentwickelt hatte 32, in dessen Dissertation „Architectural Styles and the Design of Network-based Software Architectures“ beschrieben [Fielding2000]. Nach [Tilkov2009] lässt sich REST auf fünf Grundprinzipien reduzieren: – Ressourcen mit eindeutiger Identifikation → lesbare und manipulierbare Einheiten, die mittels global gültiger und eindeutiger Adressen (URIs) identifziert werden – Unterschiedliche Repräsentationen → die Einheiten sind nach außen nur durch ihre Repräsentationen sichtbar und manipulierbar; für jede Ressource kann es eine Vielzahl an Repräsentationen geben – Verknüpfungen/Hypermedia → Benutzung von Hypertext33 zur Verknüpfung von Inhalten untereinander – Standardmethoden → ein Satz von Methoden, der auf alle Ressourcen angewendet werden kann, bildet eine einheitliche Schnittstelle – Statuslose Kommunikation → die Verantwortung für die Verwaltung des Applikationsstatus liegt beim Client, dadurch wird das System deutlich vereinfacht Bis in die 90er Jahre wurde das World Wide Web vor allem benutzt, um statische Dokumente abzurufen. Vor allem mit dem Aufkommen vieler dynamischer Websites wurde der Bedarf nach einer grundsätzlichen Theorie, einem theoretischen Fundament, das dem World Wide Web zugrunde liegt, immer größer [Tilkov2009]. Roy Fielding hat mit REST ein einheitliches Konzept für statische und dynamische Inhalte geschaffen: die Ressource. 32 Vgl. [wwwHTTP1999] 33 Die Begriffe Hypertex und Hypermedia werden oft synonym verwendet. Siehe dazu auch http://de.wikipedia.org/wiki/Hypermedia 20 Im Zusammenhang mit REST spricht man daher auch von einer Ressourcen-orientierten Architektur (ROA).34 2.3.2 Ressourcen und Repräsentationen Bei einer Ressource handelt es sich um ein abstraktes Konzept für eine Einheit oder ein Objekt. Laut [Richardson2007] ist eine Ressource „alles, was wichtig genug ist, um als eigenständiges Etwas referenziert zu werden“. Möchte ein Nutzer Informationen über eine Einheit abrufen, Änderungen an ihr vornehmen oder einen Hypertext-Verweis darauf weiterleiten, sind dies Argumente dafür, etwas als Ressource zu identifizieren. Eine Ressource kann dabei sowohl ein Dokument sein, ein reales physikalisches Objekt, ein Eintrag in einer Datenbank (der wiederum die beiden vorgenannten Beispiele abbilden könnte) oder auch eine Aufistung anderer Ressourcen. Ressourcen als solche sind nach außen nicht sichtbar. Sichtbar sind stattdessen ihre sogenannten Repräsentationen. Davon kann jede Ressource mehrere haben. Eine andere Definition einer Ressource lautet daher auch: „eine durch eine gemeinsame ID zusammengehaltene Menge von Repräsentationen“ [Tilkov2009]. Bei einer Repräsentation kann es sich zum Beispiel um ein HTML-Dokument handeln, ein PDF-Dokument, ein JSON-Dokument oder auch ein Bild. Repräsentationen werden auch benötigt, um Ressourcen zu verändern; ein Beispiel hierfür könnte ein Formular sein, mit dem man die Eigenschaften einer Ressource verändern kann. Um Ressourcen zu identifizieren werden Uniform Resource Identifer35, kurz URIs, benutzt. Hierbei handelt es sich um „Adressen“, welche global gültig und einzigartig sind. Jeder URI identifiziert hierbei genau eine Ressource. Umgekehrt können aber auch mehrere URIs auf dieselbe Ressource verweisen [wwwW3CArchitecture]. 2.3.3 Die Bestandteile von REST Bei REST handelt es sich um einen „Hybrid-Style“ für verteilte Systeme, der aus diversen netzwerkbasierten Architekturstilen abgeleitet wurde und mit zusätzlichen Einschränkungen versehen wurde [Fielding2000]. Die wichtigsten Bestandteile sollen nun hier kurz erläutert werden. 34 Genauer gesagt ist die ROA ein Weg, eine REST-konforme Architektur umzusetzen, da sie bereits Gebrauch von konkreten Konzepten wie URIs und HTTP macht [Richardson2007]. 35 Siehe auch http://de.wikipedia.org/wiki/Uniform_Resource_Identifier 21 Client-Cache-Stateless-Server Ausgangspunkt bei REST ist das Client-Server Architekturmuster 36. Als nächste wichtige Einschränkung wird festgelegt, dass die Kommunikation im System zustandslos sein soll, womit das System das „Client-Stateless-Server“ Muster umsetzt. Diese Einschränkung bedingt, dass jede Anfrage vom Client an den Server alle nötigen Informationen beinhalten muss, um die Anfrage vollständig zu verstehen. Es wird somit auf dem Server kein für die Kommunikation wichtiger Zustand gespeichert und der Client trägt die Verantwortung, diesen zu verwalten. Durch diese Einschränkung wird das System weitaus verlässlicher und skalierbarer. Da sich der Zustand auf dem Client befindet, können auch unterschiedliche Server-Maschinen die Anfragen beantworten und es macht keine Probleme, sollte einer ausfallen. Außerdem werden auf dem Server weniger Ressourcen verbraucht, wenn keine Zustände gespeichert werden müssen und die Implementierung des Servers wird deutlich einfacher. Dadurch dass zusätzlich bei jeder Datenübermittlung angegeben wird, ob die Daten cacheable37 sind, wird die Efzienz noch weiter erhöht, weil insgesamt weniger Daten übermittelt werden müssen (wenn sich diese nicht ständig ändern). Zusätzlich wird die Latenzzeit für viele Aktionen verringert. Layered-System und Code-on-Demand Zwei weitere Entwurfsmuster, die in REST mit eingefossen sind, sind das Layered-System Muster und das Code-on-Demand Muster. Beim Layered-System Muster kann die Architektur aus mehreren hierarchischen Ebenen bestehen, die jeweils nur Kenntnis von einer weiteren Ebene besitzen. Mit dieser interagieren sie. Das Code-on-Demand Muster propagiert, dass die Funktionalität des Clients dynamisch erweitert werden kann. Zusätzlicher Code wird vom Server ausgeliefert und auf dem Client ausgeführt. Dies gehört zu den Grundfunktionalitäten des World Wide Web, denn bei den meisten Websites, die im Browser betrachtet werden, wird zusätzlicher Javascript Code vom Server geladen und im Browser ausgeführt. So muss der Client von sich aus über keine Informationen verfügen, wie er die vom Server gesendeten Daten verarbeiten kann, sondern der Server liefert gewissermaßen die „Anleitung“ dazu gleich mit. Das gesamte System wird dadurch sehr fexibel und beliebig erweiterbar. Uniform Interface REST propagiert die lose Kopplung durch eine „uniforme Schnittstelle“ [Fielding2000]. Mittels dieser können Ressourcen und deren Repräsentationen abgerufen und manipuliert 36 Siehe http://en.wikipedia.org/wiki/Client-server_model 37 Siehe http://de.wikipedia.org/wiki/Cache 22 werden. Jede Ressource muss dabei den gleichen Satz von Methoden unterstützen (daher auch uniform = einheitlich). Durch die Nutzung dieser einheitlichen Schnittstelle werden Abhängigkeiten vermieden und Teile des Systems leichter austauschbar, da die Implementierung jeweils unter dem Interface verborgen ist. 2.3.4 REST, HTTP und die HTTP-Methoden Bei der Entwicklung von REST wurden die Aspekte der Implementierung ausgeblendet und stattdessen der Blick vollständig auf das System und dessen Eigenschaften als Ganzes konzentriert. In der Praxis tritt REST meist im Zusammenhang mit dem Protokoll HTTP auf38. HTTP bietet eine Schnittstelle um das REST Paradigma anzuwenden. Vielen ist HTTP nur als einfaches Protokoll zum Abrufen von Websites bekannt. In Wahrheit verfügt HTTP jedoch über mehr Fähigkeiten. Das Protokoll bietet eine Anzahl von Operationen, die auch als Verben bezeichnet werden und für alle Ressourcen gleichermaßen gültig sein sollen. Man spricht daher von der bereits erwähnten „uniformen“ Schnittstelle. Über diese Verben soll nun ein kurzer Überblick gegeben werden. GET GET ist die grundlegende und am häufigsten verwendete Operation von HTTP. Sie wird bei jeder Anfrage eines Webbrowsers nach einer Website, also einem HTML-Dokument, benutzt. Allgemeiner gesagt fragt sie eine Repräsentation einer Ressource ab. REST folgend müssen GET Anfragen vom Client beliebig wiederholt werden können ohne eine Änderung am Zustand des Servers hervorzurufen. Man sagt deshalb, dass die Methode sicher (safe) ist. Der Client fordert keine Änderung am Zustand des Servers an und geht somit auch keine Verpfichtungen ein. Ein Seiteneffekt in Form zum Beispiel eines Eintrags in eine Logdatei auf dem Server ist jedoch durchaus möglich [Tilkov2009]. Selbst Webanwendungen, die nicht das komplette REST Paradigma implementieren wollen, sollten sich an diese Regel halten, sonst könnten zum Beispiel schon einfache Webcrawler mittels Anfragen wie http://www.example.com/ressource/?action=delete große Schäden in der Anwendung anrichten [Edlich2011]. HEAD HEAD hat dieselben Eigenschaften wie GET, liefert aber statt der ganzen Repräsentation nur die Metainformationen über eine Ressource zurück. Laut Spezifikation müssen genau dieselben Daten im HTTP-Header zurück gesendet werden wie bei einer GET-Anfrage, nur dass der üblicherweise dazugehörige Daten-Body nicht mitgesendet wird. Somit kann 38 Wie oben schon erwähnt wurde der HTTP Standard maßgeblich von Roy Fielding, dem auch REST zu verdanken ist, mitgestaltet. 23 zum Beispiel die Existenz einer Ressource überprüft werden oder Informationen über den Umfang einer Ressource eingeholt werden, bevor diese wirklich übertragen wird. PUT Im Gegensatz zur Abfrage mit GET können Ressourcen mit PUT geschrieben, das heißt neu angelegt oder geändert werden. Ein PUT-Befehl überträgt eine neue Repräsentation einer Ressource oder seine geänderten Eigenschaften an den Server. Die Methode ist „idempotent“, das heißt wird der Befehl mehrere Male mit denselben Argumenten aufgerufen, so muss dies immer zum selben Zustand auf dem Server führen. POST Mittels der POST Methode überträgt der Client Daten zur Verarbeitung an den Server. Die Ergebnisse dieser Verarbeitung können zum Anlegen oder zur Änderung von Ressourcen führen oder auch komplett seiteneffektfrei sein. POST ist damit die Methode der Wahl um beliebige Funktionalitäten umzusetzen, die in der HTTP-Spezifikation nicht vorgesehen sind und in denen der Client Daten an den Server senden muss. Als Alternative zur Übertragung von Daten bietet sich noch GET an, hier müssen jedoch alle Daten komplett im URI codiert werden. Dies kann aber zu Problemen führen und die Datenmenge ist hier begrenzt. Weiterhin würde beispielsweise beim Ändern von Ressourcen die Einschränkung unter Umständen nicht mehr eingehalten werden, dass GET zu keiner Änderung am Zustand des Servers führen kann. Im Umgang mit Ressourcen nach dem REST-Ansatz hat sich etabliert, dass zwischen den beiden schreibenden Methoden PUT und POST folgendermaßen unterschieden wird: POST legt neue Ressourcen an und und PUT ändert sie39. Ein grundsätzlicher Unterschied zwischen beiden Methoden ist, dass man bei PUT die Anfrage an den URI der anzulegenden/zu ändernden Ressource sendet, während bei POST die Anfrage an den URI der für die Datenverarbeitung zuständigen Ressource gesendet wird. Der URI einer evtl. neu angelegten Ressource kann dann vom Server bestimmt werden und wird im HTTPHeader der Antwort zurück gesendet. PUT eignet sich dagegen auch für das Neuanlegen von Ressourcen unter einem durch den Client vorgegebenen URI. PATCH Bei PATCH handelt es sich um eine zusätzliche HTTP-Methode, die in [wwwHTTP2010] vorgeschlagen wurde, um Ressourcen zu ergänzen bzw. teilweise zu ändern. Mit der PUTMethode wird immer die gesamte Ressource ausgetauscht; bei Ergänzungen erfordert dies somit zunächst eine GET-Anfrage, um den aktuellen Stand der Ressource zu erfragen. 39 Vgl. z.B. Ruby on Rails Guides: http://guides.rubyonrails.org/routing.html#crud-verbs-and-actions 24 Diese Anfrage erspart man sich mit PUT, da man direkt Ergänzungen vornehmen kann ohne die Ressource als Ganzes übersenden zu müssen. Des Weiteren ist die Gefahr geringer, dass Änderungen verloren gehen, sollten mehrere Änderungen in einem kurzen Zeitraum gesendet werden.40 DELETE Wie der Name schon verrät, bewirkt DELETE die Löschung von Ressourcen. DELETE ist genau wie PUT idempotent, denn da eine Ressource nur einmal gelöscht werden kann, führen mehrere Aufrufe auf derselben Ressource zum gleichen Zustand auf dem Server. TRACE, OPTIONS und CONNECT Es gibt in der HTTP Spezifikation noch drei weitere Methoden: TRACE, OPTIONS und CONNECT. Diese spielen jedoch keine wesentliche Rolle für Webanwendungen nach dem REST-Prinzip. Sie sollen jedoch trotzdem hier kurz erwähnt werden: Mit der OPTIONS Methode können Metadaten über eine Ressource angefordert werden, unter anderem darüber, welche der HTTP-Methoden von ihr unterstützt werden. TRACE dient zur Diagnose von HTTP-Verbindungen und liefert die Anfrage genau so zurück, wie sie vom Server empfangen wurde. So kann festgestellt werden, ob sie eventuell auf dem Weg verändert worden ist. CONNECT dient zur Initiierung einer Proxy-Verbindung durch einen SSL-Tunnel. 2.3.5 HTTP-Header Außer der HTTP-Methode, dem URI und dem Datenteil (Body) bestehen HTTPAnfragen und Antworten noch aus dem weiter oben bereits erwähnten Header. Hier können Metainformationen in Form von Schlüssel/Wert-Paaren übergeben werden. Diese Paare können beliebig sein, die meisten Headerfelder sind jedoch standardisiert ([wwwHTTP1999] und [wwwHTTP2005]). Sie erlauben zum Beispiel Angaben über akzeptierte Antwortformate (Accept)41, über akzeptierte Sprachen der Antwort (AcceptLanguage) oder die Steuerung des Caching-Verhaltens, das zum Beispiel auch ganz verboten werden kann (Cache-Control: no-cache). Einige Parameter werden genutzt um konditionale GET-Anfragen zu senden, so wird einem Server zum Beispiel mittels des Headerfeldes IfModifed-Since erlaubt, bei unveränderten Daten nur einen Header mit dem Statuscode 304 (Not Modified)42 und ohne Daten zurückzusenden. So kann das unnötige Senden von 40 Siehe auch: http://www.mnot.net/blog/2012/09/05/patch 41 Um wieder den Bogen zum Konzept der Ressource zu schlagen kann man die Accept-Angabe auch so sehen: Der Client entscheidet über die Art von Repräsentation(en), die er von der adressierten Ressource zugesendet bekommen möchte. Dies kann aber auch schon durch den URI festgelegt sein, wenn dieser z.B. eine Dateiendung wie .html enthält. 42 Weitere Informationen zu den Statuscodes finden sich unter http://de.wikipedia.org/wiki/HTTPStatuscode 25 großen Datenmengen vermieden werden, wenn sich nichts an der Repräsentation geändert hat. 2.3.6 REST und NoSQL Viele NoSQL Datenbanken, darunter auch ArangoDB, bieten REST/HTTP-Schnittstellen zur Interaktion mit der Datenbank. Einige NoSQL Datenbanken verfolgen sogar intern einen REST-Entwurfsansatz. Von den in Abschnitt 2.2 vorgestellten Datenbanken zählen hierzu CouchDB, Riak und Neo4J. Bei allen diesen Datenbanken läuft der primäre ClientZugriff über REST/HTTP-Anfragen. Bei CouchDB und Neo4J gibt es dazu, ebenso wie bei ArangoDB, ein integriertes Browser-Interface zur Verwaltung der Datenbank, welches ebenfalls von der HTTP-Schnittstelle Gebrauch macht.43 Im nächsten Abschnitt wird nun näher auf die Datenbank ArangoDB eingegangen, die Gegenstand dieser Arbeit ist. Neben einer grundlegenden Beschreibung ihrer Eigenschaften wird auch ihre REST/HTTP-Schnittstelle näher untersucht. 43 Bei Riak sind ebenfalls Browser-Interfaces verfügbar, allerdings nur als Community-Projekte, die nicht standardmäßig mitgeliefert werden. 26 2.4 ArangoDB Bei ArangoDB handelt es sich um eine Multimodel Open Source NoSQL-Datenbank. Die Datenbank unterstützt die Datenmodelle Dokumente, Key/Value und Graphen. Es handelt sich somit um eine Art Mischung aus den in Abschnitt 2.2 vorgestellten Datenbanktypen. Das Projekt ArangoDB wurde im Jahr 2011 von der Firma triAGENS 44 gestartet. Im Frühling 2012 wurde Version 1.0 veröffentlicht und die Datenbank befindet sich nach wie vor in stetiger Weiterentwicklung, sodass allein während des Entstehens dieser Arbeit mehrere neue Versionen veröffentlicht wurden. Die in dieser Arbeit betrachtete Version ist 1.4.* (Version 1.4.0 wurde am 30.10.2013 veröffentlicht45). Im Folgenden sollen die Eigenschaften von ArangoDB näher untersucht werden, um eine Einordnung in das Feld der NoSQL-Datenbanken vornehmen zu können. Es soll auch versucht werden, Parallelen und Unterschiede zu den in Abschnitt 2.2 beschriebenen Datenbanken aufzuzeigen. Da es sich bei ArangoDB zuallererst um eine DokumentDatenbank handelt, soll der Vergleich vor allem mit CouchDB und MongoDB erfolgen. 2.4.1 Eigenschaften und Designziele ArangoDB wird von seinen Entwicklern als „Database for the Web“ bezeichnet und von ihnen außerdem zu einer „zweiten Generation von NoSQL-Datenbanken“ gezählt [wwwArangoTalk1]. Die Datenbank ist eine Art „Allzweckwaffe“, die möglichst viele Möglichkeiten der Anpassung an die sich stetig verändernden Anforderungen einer WebAnwendung im Laufe ihrer Entwicklung bieten soll. ArangoDB bietet unter anderem verschiedene Möglichkeiten der Skalierung, verschiedene Konsistenzmodelle, verschiedene Möglichkeiten der Abfrage von Daten sowie verschiedene Möglichkeiten der Indizierung von Dokumentattributen. Sollte sich beispielsweise während der Entwicklung einer Anwendung herausstellen, dass auch die Möglichkeit des Durchsuchens von Datensätzen nach der geografischen Lage benötigt wird, kann in ArangoDB auch eine Indizierung nach Geokoordinaten erfolgen [wwwArangoTalk2]. Ebenso sind Möglichkeiten der Datenreplikation vorhanden, es wird aber ausdrücklich nicht versucht, eine horizontale Skalierbarkeit in Dimensionen, wie sie zum Beispiel mit Riak oder MongoDB möglich ist, umzusetzen.46 44 http://de.triagens.com/ 45 https://www.arangodb.org/2013/10/30/arangodb-1-4-0-released 46 Hier sollte aber erwähnt werden, dass ArangoDB ab Version 2 auch das sogenannte Sharding unterstützt, womit eine horizontale Skalierung möglich wird. Siehe dazu auch Kapitel 7 „Fazit und Ausblick“. 27 Eine der Hauptstärken von ArangoDB liegt in der Verfügbarkeit der verschiedenen Datenmodelle. So muss nicht gleich auf eine zusätzliche Datenbank zurückgegriffen werden, sollte sich beispielsweise während der Entwicklung einer Anwendung herausstellen, dass nicht nur Dokumente, sondern auch Graphenstrukturen abgespeichert werden sollen. Insbesondere bietet ArangoDB daher die Möglichkeit, das Konzept der Polyglot Persistence mit nur einer Datenbank umzusetzen, sodass der technische Verwaltungsaufwand viel geringer ist als beim Einsatz mehrerer Datenbank-Systeme. ArangoDB erlaubt in erweitertem Maße die Benutzung von JavaScript um mit der Datenbank zu arbeiten, nicht nur auf der Client- sondern auch auf der Server-Seite. Hierzu wird auf der Server-Seite von Googles JavaScript-Engine V8, die auch in Google Chrome zum Einsatz kommt, Gebrauch gemacht. Der Kern von ArangoDB ist in C/C++ sowie teilweise auch in JavaScript implementiert. Der Server von ArangoDB arbeitet mit mehreren nebenläufig ausgeführten Threads und ist für die Ausführung auf Multiprozessor-Systemen optimiert [ArangoDBBlog2]. DatenbankAnfragen können somit von mehreren Threads gleichzeitig bearbeitet werden. ArangoDB unterstützt „blocking“ und „non-blocking“ Requests. Bei der optionalen Nutzung von non-blocking Requests werden die Requests auf dem Server in einer Queue gespeichert; der Client muss dann nicht auf eine Antwort des Servers warten, sondern kann gleich weitere Anfragen schicken. Eine Besonderheit von ArangoDB sind die sogenannten „schema-free Schemata“. Gleichartige Dokumente in einer Collection werden von ArangoDB automatisch erkannt und platzefzient abgespeichert. Gleichzeitig hat man weiterhin die Freiheit, komplett schemafrei zu arbeiten [ArangoDBBlog1]. Bei diesem Ansatz werden die Vorteile der Schemafreiheit einer Dokumenten-Datenbank kombiniert mit den Vorteilen von Schemata, wie sie aus relationalen Systemen bekannt sind. 2.4.2 Speichereffizienz und Performance ArangoDBs Design ist laut seiner Entwickler nicht auf Performance ausgelegt, sondern auf vielseitige Anwendungsmöglichkeiten. Trotzdem erhält die Datenbank bei Performanceund Speicherverbrauchs-Vergleichen mit anderen Datenbanken durchaus gute Werte. Laut den Tests in [wwwArangoBlog3] verbraucht ArangoDB bei einer großen Anzahl an Datensätzen in der Datenbank durchweg weniger Speicherplatz als MongoDB. Dies ist wohl vor allem der impliziten Schema-Erkennung (schema-free Schemata) bei ArangoDB 28 zu verdanken. Bei MongoDB und CouchDB werden die Strukturinformationen für jedes Dokument redundant abgespeichert, während bei ArangoDB die Struktur bei gleich aufgebauten Dokumenten automatisch erkannt wird und pro Collection nur einmal gespeichert werden muss. Bei CouchDB ist dagegen aber eine Kompression der Daten möglich, womit der benötigte Speicherplatz auch teilweise unter dem Niveau von ArangoDB liegen kann. Da bei ArangoDB alle Kommunikation zwischen Server und Client über HTTP-Requests läuft, ist die Geschwindigkeit des HTTP-Layers der Datenbank von entscheidender Wichtigkeit für die Gesamt-Performance der Datenbank. In [wwwArangoBlog4] wurde die HTTP-Performance von ArangoDB in einigen Benchmark-Tests mit der einiger gängiger Webserver verglichen47. Es zeigte sich, dass die Performance von ArangoDB im FileserverModus mit der gängiger Webserver mithalten kann. In Testfällen mit einer sehr hohen Anzahl an gleichzeitig geöffneten Client-Verbindungen schlägt die Performance von ArangoDB sogar die der anderen Webserver. Der die Performance betreffende nächste „Konkurrent“ war hierbei der Webserver nginx, der bei wenigen gleichzeitig geöffneten Verbindungen teilweise mehr Anfragen pro Sekunde beantworten konnte. Die HTTP-Performance von ArangoDB kann außerdem noch verbessert werden durch die Nutzung von Batch-Requests. Hierbei werden mehrere Datenbank-Anfragen im Body einer HTTP-Anfrage übermittelt. Laut [wwwArangoBlog5] kann dadurch die benötigte Zeit für das Einfügen und Ändern von Dokumenten um 80% reduziert werden 48. Im Vergleich mit MongoDB liegt die Performance von ArangoDB bei Batch-Requests meist nah an der von MongoDB; wobei ArangoDB bei großen Datensätzen auch schneller sein kann als MongoDB. Die Bearbeitungszeit von Batch-Requests bei CouchDB betrug in vielen Tests ein Vielfaches derer von ArangoDB und MongoDB [wwwArangoBlog6].49 2.4.3 Concurrency, Transaktionen, Skalierbarkeit und Replikation Als Concurrency-Control Strategie kommt bei ArangoDB Append-Only/MVCC (letzteres steht für „Multi-Version-Concurrency-Control“) zum Einsatz. Hierbei wird ein Dokument bei einem Schreibvorgang nicht blockiert, sondern eine neue Version des Dokumentes 47 Hierbei handelte es sich ausdrücklich nur um vergleichende Tests, d.h. es bestand kein Interesse, die absolute Performance der Produkte zu messen. 48 Dies ist jedoch abhängig vom Use-Case. Es ergeben sich vor allem Vorteile wenn eine größere Anzahl an Requests mit jeweils wenig Daten gesendet werden. 49 Zu beachten ist hier einerseits, dass MongoDB nicht mit dem HTTP-Protokoll, sondern mit einem binären Protokoll arbeitet, was der Datenbank einen initialen Performance-Vorsprung gibt; Andererseits, dass in den Tests die Daten-Kompression bei CouchDB ausgeschaltet war, um die Performance zu erhöhen, womit dann gleichzeitig die Vorteile des niedrigen Speicherverbrauchs von CouchDB verloren gingen. 29 inklusive einer Versionsnummer erzeugt. So kann ein Lesezugriff zu jeder Zeit erfolgen, auch wenn das Dokument gerade geändert wird. Ältere Versionen des Dokumentes werden dann von einem Garbage-Collection Prozess regelmäßig gelöscht [ArangoDBBlog2]. Bei ArangoDB sind hierdurch auch konditionale Schreibvorgänge möglich, wie zum Beispiel „ändere ein Dokument nur, wenn die letzte Version die Versionsnummer 123456 hat“. Dieses Vorgehen entspricht in etwa der Strategie bei CouchDB. Hier können im Gegensatz zu ArangoDB jedoch auch mehrere konkurrierende Schreibvorgänge gleichzeitig durchgeführt werden; eventuell entstehende Konfikte werden dann in einem MergeProzess beseitigt [Edlich2011]. Der MVCC-Ansatz erinnert zudem auch an die Concurrency-Strategie von Clojure. Hier gibt es unveränderliche Datenstrukturen, die nur durch das Erstellen einer neuen, aktualisierten Version „geändert“ werden können. Hierdurch wird ein sicheres nebenläufiges Arbeiten möglich. Nicht mehr benötigte ältere Versionen der Daten werden daraufhin von der Garbage-Collection50 der Java Virtual Machine gelöscht. Seit Version 1.3 unterstützt ArangoDB zusätzlich auch ACID-Transaktionen. ACID steht für „Atomic, Consistent, Isolated, and Durable“. Dies bedeutet, dass jede Transaktion entweder vollständig ausgeführt wird oder gar keinen Effekt hat. Erst bei einem erfolgreichen Abschluss einer Transaktion wird das Ergebnis nach außen sichtbar und dann persistent abgespeichert. Bei ArangoDB werden die Transaktionen als Ganzes an den Server geschickt, dort ausgeführt und anschließend eine Meldung an den Client über Erfolg oder Misserfolg der Transaktion gesendet. Dieses Vorgehen unterscheidet sich von Transaktionen in SQL, wo auch während einer Transaktion Kommunikation zwischen Client und Server stattfinden kann [wwwArangoFAQ]. ArangoDB wird von seinen Entwicklern auch als „mostly-memory database“ bezeichnet. Damit gemeint ist, dass die Performance besonders gut ist, wenn die gesamten Daten in den RAM der ausführenden Maschine passen und die Datenbank nicht gezwungen ist, Daten zwischen RAM und Festplatte hin- und her zu kopieren [wwwArangoFAQ]. Um die Festplatten-Synchronisation zu kontrollieren, bietet ArangoDB die zwei Optionen „eventual“ und „immediate“. Diese beiden Optionen können jeweils pro Collection dauerhaft konfiguriert werden, sowie auch bei jeder Aktion individuell. Bei „immediate“ wird die Änderung sofort nicht nur im RAM gespeichert, sondern auch auf der Festplatte gesichert. Erst wenn dies erfolgreich geschehen ist, sendet der Server eine 50 Vgl. http://en.wikipedia.org/wiki/Garbage_collection_(computer_science) 30 Antwort an den Client. Bei „eventual“ wird dagegen die Änderung zunächst nur im RAM durchgeführt. Die Synchronisation mit der Festplatte erfolgt dann erst später im Hintergrund. Insgesamt bedeutet dies einen Gewinn an Geschwindigkeit, da weniger System-Anfragen durchgeführt werden. Dieses Vorgehen kann jedoch auch zu Datenverlusten führen, sollte es beispielsweise zu einem Absturz des Systems kommen [wwwArangoFAQ]. Als Strategie für die Replikation bietet ArangoDB die sogenannte asynchrone Master/Slave-Replikation. Hierbei können die Datenbanken jeweils als Master oder als Slave konfiguriert werden. Der Client kann dann Leseanfragen an alle Datenbanken senden, Schreibanfragen jedoch nur an den Master. Änderungen am Master können daraufhin von den Slaves aus dessen Log gelesen werden und jeweils auf die eigenen Daten angewendet werden. Die Daten sind somit „eventual consistent“ [wwwArangoManRep]. Der Gewinn bei der Master/Slave-Replikation liegt in der „Lese-Skalierung“ und außerdem in der Möglichkeit von „Hot Backups“. Es sollen aber laut der Entwickler bei ArangoDB bewusst keine Features für unbegrenztes horizontales Skalieren umgesetzt werden. Generell ist die Datenbank dafür konzipiert, dass alle Daten auf einen Server passen. In der Abwägung zwischen Skalierbarkeit und guten Abfragemöglichkeiten geht ArangoDBs Design eher in Richtung der Abfragemöglichkeiten. Diese Entscheidung steht im Gegensatz beispielsweise zum Design von Riak, dessen Abfragemöglichkeiten nicht sehr umfangreich sind (Key/Value, Volltext-Suche und Map/Reduce). Dafür ist die Datenbank aber hochskalierbar. 2.4.4 ArangoDBs HTTP/REST-Interface ArangoDB kommuniziert mit der Außenwelt, das heißt mit allen Clients, durch sein HTTP-Interface. Dieses unterstützt die HTTP-Methoden GET, POST, PUT, DELETE und PATCH sowie die HTTP-Versionen 1.0 und 1.1, wobei Antworten vom Server jedoch immer in Version 1.1 erfolgen. Daten werden im Body des HTTP-Requests im JSONFormat an den Server gesendet und werden ebenfalls ausschließlich als JSON von diesem zurückgegeben. Des Weiteren werden sowohl Standard- als auch einige Custom-Header Parameter sowie einzelne Parameter auch als URI-Parameter unterstützt. 2.4.5 Datenbanken, Collections, Dokumente und Graphen in ArangoDB Im Folgenden sollen kurz die Eigenschaften der in ArangoDB vorhandenen Datenstrukturen zur Organisation von Daten erläutert werden. 31 Datenbanken Datenbanken stellen in ArangoDB die oberste Hierarchieebene zur Organisation von Daten dar. Jeder ArangoDB Server kann mehrere Datenbanken enthalten, wobei immer mindestens eine Datenbank namens _system vorhanden ist, die „System-Datenbank“ genannt wird. Jede Datenbank besteht aus Collections und datenbankspezifischen WorkerProzessen [wwwArangoAPIDB]. Die Collections unterteilen sich in User-Collections, die vom User selbst erstellt werden und System-Collections, die interne Informationen enthalten, wie beispielsweise Angaben über User und über Replikation. Werden Aktionen über die REST/HTTP-API ausgeführt, so erfolgen diese immer im Kontext einer Datenbank. Die Adresse einer API-Ressource wird üblicherweise wie folgt aufgebaut: http://server:port/_db/<database-name>/...<API-method> zum Beispiel: http://localhost:8529/_db/mydb/...<API-method> Die Datenbank muss jedoch nicht in jeder URI explizit angegeben werden. Wenn keine Datenbank angegeben ist, wird die Aktion standardmäßig im Kontext der SystemDatenbank ausgeführt. Weiterhin gibt es ein sogenanntes „Database-to-Endpoint Mapping“, das heißt für jeden Port kann eine Liste von Datenbanken angegeben werden, die über diesen Port erreichbar sind. Wird eine API-Anfrage an einen Port gesendet, für den das Database-to-Endpoint Mapping konfiguriert ist, so wird standardmäßig die erste Datenbank in dieser Liste verwendet. Auf die API-Methoden zur allgemeinen DatenbankKonfiguration kann nur im Kontext der _system Datenbank zugegriffen werden. Collections Collections entsprechen vom Konzept her in etwa den Tabellen der relationalen Datenbanksysteme und stellen in ArangoDB die nächste Hierarchieebene unter den Datenbanken dar. Genau wie auch in MongoDB können Dokumente in ArangoDB nur innerhalb einer Collection existieren. In CouchDB dagegen werden die Dokumente direkt auf der Datenbank-Ebene abgelegt. Collections können und sollten benutzt werden, um Dokumente logisch zu gruppieren. ArangoDB bietet hier die bereits erwähnte Besonderheit der impliziten Schema-Erkennung, das heißt je größer die Ähnlichkeiten in der Struktur von Dokumenten innerhalb einer Collection, desto platzefzienter können diese abgespeichert werden. 32 Collections können zwei Typen haben: document, wobei die Collection dann normale Dokumente speichert, sowie edge; In letzterem Fall kann die Collection als Edge-Collection für einen Graphen dienen (siehe unten). Dokumente Dokumente in ArangoDB sind JSON-Objekte, die Listen enthalten können und unendlich tief geschachtelt werden können [wwwArangoAPIDoc]. Jedes Dokument wird eindeutig identifiziert durch sein Document-Handle in der Form myusers/2345678. Der Teil vor dem Schrägstrich ist hierbei der Name der Collection und der Teil hinter dem Schrägstrich der Document-Key. Das Document-Handle wird in jedem Document unter dem Key „_id“ abgespeichert und der Document-Key selber noch einmal unter dem Key „_key“51. Beim Document-Key handelt es sich um einen in der Collection einzigartigen Key, den der Benutzer bei der Erstellung des Dokumentes selber angeben kann, oder der automatisch über einen auf Collection-Ebene konfigurierbaren Key-Generator erstellt wird. Als drittes vom System kommendes Attribut gibt es noch die Revisions-Nummer, die unter „_rev“ abgespeichert wird. Die Attribute _id und _key sind unveränderbar, sobald das Dokument einmal erstellt wurde. Die Revisions-Nummer identifiziert die Version des Dokumentes und wird jeweils neu zugewiesen, wenn das Dokument geändert wurde. Alle Dokumente in einer Collection vom Typ edge haben außerdem jeweils ein „_from“ und ein „_to“ Attribut. Diese enthalten jeweils einen Key eines anderen Dokumentes, auf das verwiesen wird. Der Dokument-Zugriff über die HTTP-API erfolgt mit einem URI im Format: http://server:port/_db/<database-name>/_api/document/<document-handle> zum Beispiel: http://localhost:8529/_db/mydb/_api/document/demo/362549736 Graphen Zur Repräsentation von Graphenstrukturen wird auf die bisher bereits vorgestellten Strukturen zurückgegriffen. Graphen bestehen aus jeweils einer Collection vom Typ edge, die die Kanten des Graphen enthält, sowie einer Collection vom Typ document, die die Knoten des Graphen enthält. Als Knoten dienen somit normale Dokumente und als Kanten die Dokumente der Collection vom Type edge, die mit ihren zusätzlichen Attributen _to und _from jeweils eine Verbindung zwischen zwei Knoten repräsentieren. 51 Bei den Dokument-Attributen, die mit einem Unterstrich beginnen, handelt es sich in ArangoDB um reservierte Keys [wwwArangoAPINaming]. 33 2.4.6 Querying In ArangoDB gibt es vier unterschiedliche Methoden der Abfrage und des Findens von Dokumenten [wwwArangoTalk1]. Die einfachste Methode ist die Abfrage mit einem bekannten Document-Key. Ist der Key und damit das genaue Dokument nicht bekannt, hat man grundsätzlich zwei Arten der Suche zur Auswahl. Für einfache Suchen bietet sich das „Querying by Example“ an, bei dem ein Beispiel-Dokument an den Server gesendet wird. Dieses enthält die Attribute, nach denen gesucht werden soll. Der Server gibt dann alle Dokumente zurück die diese Attribute ebenfalls enthalten. Diese Art der Suche kann immer nur im Kontext einer Collection durchgeführt werden. Für komplexere Suchanfragen, bei denen auch Joins über mehrere Collections hinweg möglich sind, bietet ArangoDB seine eigene Query-Sprache namens AQL (für „ArangoDB Query Language“). Diese weist Ähnlichkeiten zu JSONiq 52 und zu SQL auf. Es wurden aber explizit andere Keywords als bei SQL verwendet, um eine Verwechslung der beiden Sprachen durch den Benutzer zu vermeiden [wwwArangoTalk1]. Zusätzlich zu Joins werden in AQL außerdem Helper-Funktionen angeboten wie for-in-Schleifen und StringVerkettungen53. Die Ergebnisse der Anfragen werden als Cursor zurückgegeben, über die iteriert werden kann; das heißt es werden nicht alle gefundenen Daten auf einmal zurückgegeben. Zum Vergleich: Suchanfragen bei CouchDB sind nur per Map/Reduce möglich, was recht aufwendig ist, da dies immer die Programmierung von JavaScript-Funktionen verlangt. Bei MongoDB erfolgt die Abfrage immer per JSON, was für komplizierte Suchanfragen schnell unübersichtlich werden kann. Daher haben sich die Entwickler von ArangoDB für die Entwicklung einer eigenen Abfragesprache entschieden. Für Suchanfragen, die so komplex sind, dass sie auch durch AQL schwer auszudrücken sind, bietet sich weiterhin noch das Formulieren von Anfragen in Form von eigenem JavaScript Code an. Dieser kann in Form von Graph-Traversierungen an den Server gesendet werden oder auch über Foxx (siehe unten) als Ressource bereit gestellt werden. 52 http://www.jsoniq.org/ 53 Für Beispiele von AQL-Abfragen siehe: https://www.arangodb.org/manuals/current/AqlExamples.html 34 2.4.7 Indizierung Für die Indizierung von Dokumenten und damit die Beschleunigung der Suche nach Dokumenten gibt es bei ArangoDB verschiedene Möglichkeiten: Hash Indices werden benutzt, um Dokumente nach Beispiel zu durchsuchen (Querying by Example). Sie können für ein oder mehrere Attribute des Dokuments erstellt werden und beschleunigen dann die Suche nach diesen Attributen. Für den Document-Key werden jeweils automatisch Hash Indices erstellt, um eine Abfrage nach diesem zu ermöglichen [wwwArangoManIndex]. Die Abfragezeit beträgt dann O(1). Mit Hash Indices können nur Suchen mit Überprüfung auf die Gleichheit von Attributen durchgeführt werden. Sollen zusätzlich Überprüfungen auf die Zugehörigkeit zu Wertebereichen stattfinden, können sogenannte Skip List Indices benutzt werden. Fulltext Indices können benutzt werden, um nach Wörtern in Attributen mit Textinhalten zu suchen. Hat man es mit Attributen für Geo-Locations zu tun, so werden auch Geo Indices zur Umkreissuche mit Längen- und Breitengraden unterstützt. Für die Suche nach Verbindungen in Graphenstrukturen werden in Edge Collections automatisch Edge Indices erstellt. Außerdem werden noch Bit-Array Indices54 unterstützt. 2.4.8 Die Bestandteile von ArangoDB ArangoDB kommt mit einer Vielzahl an mitgelieferten Tools. Diese sollen hier kurz beschrieben werden. 2.4.8.1 Die Kommandozeilen-Tools Bei arangod, arangosh und arangoimp handelt es sich um Kommandozeilen-Tools, die grundsätzliche Funktionen der Datenbank zur Verfügung stellen. arangod steht für „Arango Daemon“. Hierbei handelt es sich um den eigentlichen Datenbank-Server, der als Deamon Prozess ausgeführt wird. Clients können zu ihm eine Verbindung via TCP/HTTP aufnehmen. arangosh steht für „Arango Shell“. Hierbei handelt es sich um eine interaktive JavaScriptShell, die alle Operationen zur Konfiguration, Manipulation und Abfrage der Datenbank unterstützt. arangoimp steht für „Arango Import“. Hierbei handelt es sich um ein Import-Tool für Datensätze. Es können im einfachsten Fall eine Anzahl von Datensätzen, die bereits im 54 Siehe hierzu http://en.wikipedia.org/wiki/Bitmap_index 35 JSON-Format vorliegen, in eine Collection importiert werden. Auch Daten im CSV 55 Format werden unterstützt. Weitere Tools sind arangodump zur Erstellung von Backups, arangorestore zur Wiederherstellung von Backups, foxx-manager zur Verwaltung von Foxx Applications (siehe 2.4.8.3) sowie arango-dfdb zum Debuggen von Datafiles und arangob für Benchmark-Tests. Letztere sind hauptsächlich zur Verwendung während der Entwicklung von ArangoDB gedacht [wwwArangoFS]. 2.4.8.2 Browser-Interface In ArangoDB mitgeliefert wird ein Browser-Interface namens Aardvark. Dieses bietet Funktionen zum Durchsuchen und Verwalten von ArangoDB. Hiermit ist das Verwalten von Datenbanken, Collections und Dokumenten auch komfortabel per User-Interface möglich. Es können alle Inhalte eingesehen werden, sowie neue Dokumente, Collections und Datenbanken angelegt werden. Graphen können in einer interaktiven Graph-Ansicht traversiert und durchsucht werden. Außerdem können Foxx-Anwendungen verwaltet werden (siehe 2.4.8.3). Weiterhin können über das Browser-Interface Statistiken (User-Time, Speicher-Auslastung etc.) sowie das Logging der Datenbank eingesehen werden. Es gibt einen Editor, mit dem AQL-Queries formuliert und abgeschickt werden können. Dieser bietet auch Templates, um das Erstellen von Queries zu vereinfachen. Außerdem steht ein KommandozeilenInterface zur Verfügung, das dieselben Funktionalitäten wie das Tool arangosh bietet. Hier können jedoch aufgrund von Browser-Einschränkungen nicht alle Befehle genutzt werden, zum Beispiel keine Befehle, welche Betriebssystem-Aufrufe bedingen. Das BrowserInterface bietet außerdem eine Übersicht über die ArangoDB HTTP-API inklusive der Möglichkeit des Absendens und Testens von Requests mit verschiedenen Optionen. 2.4.8.3 Foxx Bei Foxx handelt es sich um ein Framework, welches es zulässt, eigenen JavaScript Code auf der Datenbank zu hinterlegen und auszuführen. Dieser Code kann zum Beispiel eine komplexe Datenbankabfrage sein oder auch eine ganze Anwendung beinhalten, wie etwa ein Content-Management-System. Foxx Anwendungen können komfortabel mit dem Tool foxx-manager aus einem zentralen Repository 56 installiert werden und zusätzlich auch im ArangoDB Browser-Interface verwaltet werden. 55 http://de.wikipedia.org/wiki/CSV_(Dateiformat) 56 https://github.com/triAGENS/foxx-apps/ 36 Durch Foxx kann ArangoDB als Application Server genutzt werden. Jede Foxx Anwendung wird auf dem Datenbank-Server unter einem eigenen URI bereit gestellt. Mit Hilfe von Controllern können dann auch eigene REST-APIs innerhalb von ArangoDB definiert werden. So kann beispielsweise auch eine ganze Single Page Webanwendung ausgeführt werden ohne ein zusätzliches zwischengeschaltetes Web-Framework und mit JavaScript als einziger Programmiersprache [wwwArangoFoxx]. Die Foxx-Technologie lässt sich vergleichen mit den CouchApps in CouchDB. Mit dem Unterschied jedoch, dass CouchApps vor allem darauf ausgerichtet ist, auch direkt HTML an den Browser auszuliefern, anstatt nur JSON wie bei Foxx. 37 3. Aufgabenstellung Ziel dieser Abschlussarbeit ist die Entwicklung eines Treibers (oder auch Clients 57) für die Datenbank ArangoDB in der Programmiersprache Clojure. Dieser soll in seiner vollständigen Implementierung die gesamten Funktionalitäten der ArangoDB REST/HTTP-Schnittstelle umfassen, so wie sie im „Implementor Manual“ auf der Website von ArangoDB58 dokumentiert ist. Der Treiber soll durch die Nutzung einer HTTP-Library Anfragen an die HTTPSchnittstelle von ArangoDB senden und die Antworten entgegennehmen und in einem geeigneten Format an den Benutzer, bzw. an das aufrufende Programm zurückgeben. Zu den zu sendenden Anfragen zählen unter anderem Anfragen an die Administrationsfunktionen von ArangoDB, Anfragen an die Funktionen zur Verwaltung von Datenbanken, Collections und Graphen sowie Anfragen zur Erzeugung und Änderung von Dokumenten. Besondere Ziele bei der Umsetzung sind: • das Schaffen einer konsistenten, einfach zu verstehenden Schnittstelle (API 59) für den Treiber; • die vom Benutzer verwendeten Funktionen sollen dabei sowohl intuitiv einsetzbar als auch vielseitig verwendbar in Bezug auf Zusatzoptionen sein; → Damit gemeint ist, dass eine Funktion zum Datenbankzugriff unter Zuhilfenahme von Default-Werten mit einem möglichst kurzen Funktionsaufruf ausführbar sein soll; sollte der Benutzer jedoch zusätzliche Optionen angeben wollen, so soll dies in einer möglichst fexiblen Art möglich sein, ohne ihn jedoch zu verwirren. • der Treiber soll möglichst zustandslos sein, da die verwendeten Technologien REST/HTTP und Clojure ebenfalls zustandslos sind; stehen Benutzungskomfort und Zustandshaftigkeit in Konkurrenz, so können jedoch aus Gründen des Benutzungskomforts auch Ausnahmen gemacht werden; • der Treiber soll möglichst leichtgewichtig sein; er soll mit möglichst geringem Overhead die Anfragen an den ArangoDB Server weiterleiten; 57 Die beiden Begriffe werden in dieser Arbeit synonym verwendet. 58 http://www.arangodb.org/manuals/current/ImplementorManual.html 59 Die Abkürzung API sowie die Worte „Schnittstelle“ und „Interface“ werden ebenfalls in dieser Arbeit weitgehend synonym verwendet; wobei jedoch die Schnittstelle des zu entwickelnden Treibers Clarango, d.h. der Satz an Methoden der durch den Benutzer aufgerufen wird, von nun an immer als „API“ bezeichnet werden soll. 38 4. Entwurf und Implementierung Bei der Entwicklung von Clarango wurde ein agiler Ansatz verfolgt. Das bedeutet, die Software wurde in vielen kleinen Schritten entwickelt und in ihren Funktionalitäten erweitert und verbessert. Im Gegensatz zum klassischen Ansatz bei der Softwareentwicklung wurde die Software nicht komplett fertig entworfen, bevor mit der Umsetzung begonnen wurde. Dies bot den Vorteil, direkt mit der Entwicklung von Clarango beginnen zu können, erste Features umsetzen zu können und sich nicht vorher „in Details verstricken“ zu müssen. Denn oft sind die Anforderungen an das Design einer Software zu Beginn noch nicht vollständig klar. So war es auch bei Clarango. Erst während der Entwicklung wurde klar, welche Anforderungen die Software genau erfüllen muss. Dies hängt vor allem mit den Anforderungen des ArangoDB REST/HTTP-Interfaces zusammen. Hier wurde zunächst experimentiert, wie und mit welchen Features der Sprache Clojure sich am besten eine Anfrage an das ArangoDB Interface zusammensetzen lässt und in welcher Form die Rückgabewerte zurückgegeben werden sollen. Der agile Ansatz bot zudem den Vorteil, dass bei jeder Fertigstellung einer Funktion wieder eine lauffähige und testbare Version der Software verfügbar war. Da auf den klassischen Ansatz der Trennung von Entwurf und Implementierung verzichtet wurde, soll auch im Text dieser Arbeit keine Trennung der beiden Bereiche erfolgen. Daher trägt dieses Kapitel den Titel „Entwurf und Implementierung“. Zu Beginn dieses Kapitels sollen kurz die Werkzeuge und Libraries, die bei der Entwicklung von Clarango eingesetzt wurden, aufgezählt und erläutert werden. Anschließend sollen die APIs einiger anderer Clojure NoSQL Treiber untersucht werden, um danach unter Einbeziehung einiger grundsätzlicher Überlegungen eine API für Clarango entwickeln zu können, die sich möglichst an bereits existierenden Clojure-Treibern orientiert. Anschließend sollen einige Implementierungsdetails sowie die Architektur von Clarango als Ganzes erläutert werden. 39 4.1 Werkzeuge 4.1.1 Versionsverwaltung Als verteiltes Versionsverwaltungs-Werkzeug kommt Git 60 und der dazu gehörige HostingDienst GitHub61 zum Einsatz. Bei Git und GitHub handelt es sich um einen de facto Standard bei Open Source Projekten. Git ist die eigentliche Versionsverwaltungssoftware, die lokal auf dem Rechner des Entwicklers ausgeführt wird. Die sogenannten Git Repositories, die den Code von Softwareprojekten enthalten, können dann im Falle von Open Source Software kostenlos auf GitHub gehostet werden. Dies erleichtert die Zusammenarbeit zwischen Entwicklern. Auf GitHub kann der gesamte Code in allen Versionen komfortabel durchsucht werden. Außerdem gibt es Readme-Dateien, die eine erste Einführung in die Software bieten sowie kostenlose „Github Pages“, auf denen eine ausführliche Dokumentation eines Projektes bereitgestellt werden kann. Das Github Repository von Clarango mit dem gesamten Code der Software findet sich unter dieser Adresse: https://github.com/edlich/clarango 4.1.2 Clojure Projektmanagement Als Projektmanagement-Tool kommt Leiningen62 zum Einsatz. Leiningen ist der QuasiStandard für das Management von Clojure-Projekten. Das Tool gibt eine Projektstruktur vor, verwaltet und lädt automatisch Abhängigkeiten aus öffentlichen Repositories und kann die Anwendung sowie dazugehörige Tests mit einem einzigen Kommando ausführen 63. Ebenso ist der automatische Upload von Projekten als Libraries in öffentliche Repositories möglich, so dass diese wiederum von anderen Leiningen Projekten geladen und verwendet werden können. 60 61 62 63 http://git-scm.com/ http://www.github.com http://leiningen.org/ Für detailliertere Informationen siehe https://github.com/technomancy/leiningen/blob/stable/doc/TUTORIAL.md 40 4.2 verwendete Libraries 4.2.1 clj-http clj-http64 ist eine Clojure-Library, die dazu dient, HTTP-Anfragen zu senden und die Antworten zu empfangen. Die Library funktioniert als Wrapper der Apache HttpComonents Library65 für Java. 4.2.1 Cheshire Bei Cheshire66 handelt es sich um eine Library zum Enkodieren und Dekodieren von JSON-Objekten. Clojure Maps können damit einfach zu JSON-Strings konvertiert werden und umgekehrt. Hierbei werden Clojure Keywords, die in den Maps als Key dienen, automatisch zu Strings umgewandelt und umgekehrt. Cheshire unterstützt alle StandardDatenstrukturen von Clojure sowie einige weitere Java-Datenstrukturen. 64 https://github.com/dakrone/clj-http 65 http://hc.apache.org/ 66 https://github.com/dakrone/cheshire 41 4.3 Versionierung Git und GitHub bieten die Möglichkeit, verschiedene Entwicklungsstände einer Software mit Tags zu markieren und in sogenannten Releases zu veröffentlichen. Releases bieten die Möglichkeit, den Stand einer Software mit einer Überschrift sowie einer weiteren Beschreibung zu versehen und als Download bereitzustellen67. Zur Versionierung wurde das in [wwwVersioning] erläuterte System verwendet. Dieses schlägt vor, Versionsnummern in drei Teile nach folgendem Muster zu unterteilen: „MAJOR.MINOR.PATCH“. Bei den drei Teilen handelt es sich jeweils um ganze Zahlen, die nur herauf, nicht herabgesetzt werden können und nach den folgenden Regeln heraufgesetzt werden: – die MAJOR-Nummer sollte sich nur ändern, wenn zur Vorgängerversion inkompatible Änderungen an der API vorgenommen wurden – die MINOR-Nummer ändert sich, wenn Funktionen hinzugefügt wurden, die äbwartskompatibel zu Vorgängerversionen sind – die PATCH-Nummer ändert sich, wenn Bug-Fixes vorgenommen wurden; diese müssen ebenfalls äbwartskompatibel sein Die während der Entstehung dieser Arbeit fertiggestellten Releases von Clarango sind unter dieser Adresse einzusehen: https://github.com/edlich/clarango/releases Eine erste Version 0.1.0 wurde veröffentlicht, als die umfangreichen Document-CRUD 68 Funktionen im document Namespace von Clarango fertiggestellt wurden. Version 0.2.0 erhielt dann zusätzlich Query-Funktionalitäten. Die zuletzt während der Entstehung dieser Arbeit fertiggestellte Version von Clarango ist 0.3.2 69. Sie enthält zusätzlich GraphFunktionalitäten sowie einige Bug-Fixes. Da die API von Clarango noch nicht als stabil betrachtet wird und sich noch in der Entwicklung befindet, wurde noch keine Version 1.0.0 veröffentlicht. Die Releases von Clarango wurden mit Leiningen in das Open Source Repository Clojars 70 hochgeladen und eine Informationsseite ist dort unter folgender Adresse zu erreichen: https://clojars.org/clarango 67 68 69 70 Für weitere Informationen siehe auch https://help.github.com/articles/about-releases CRUD ist eine Abkürzung für „Create, Read, Update, Delete“ https://github.com/edlich/clarango/releases/tag/v0.3.2 https://clojars.org/ 42 4.4 Vergleich von APIs anderer Clojure Datenbanktreiber Um die in Kapitel 3 aufgeführten Anforderungen an die Schnittstelle von Clarango möglichst gut umsetzen zu können, sollen nun einige APIs anderer ClojureDatenbanktreiber untersucht werden. Die gesammelten Informationen werden dann später genutzt, um eine API für Clarango zu entwickeln, die sich möglichst „natürlich“ anfühlt. Unter anderem weil sie sich an bereits existierenden Treibern orientiert. Untersucht werden sollen Treiber für die Dokumenten-Datenbanken MongoDB, CouchDB und Elasticsearch. Ein MongoDB Client eignet sich hierbei besonders gut zum Vergleich, da bei MongoDB die Dokumente ebenso wie bei ArangoDB in Collections organisiert sind [wwwMongoDBCRUD]. Andererseits soll auch ein CouchDB Client untersucht werden, da das Design von CouchDB durch dessen REST-Ansatz Gemeinsamkeiten mit dem von ArangoDB aufweist. Weiterhin sollen noch ein Treiber für die Multimodel-Datenbank OrientDB und ein Treiber für die Key/Value-Datenbank Redis untersucht werden, um auch den Multimodel- und Key/Value-Aspekt von ArangoDB abzudecken. Um auch Graph-Funktionalitäten und den Einsatz einer eigenen QuerySprache einzubeziehen, wird zuletzt noch der Treiber Neocons für die Graph-Datenbank Neo4J untersucht. 4.4.1 Monger für MongoDB Bei Monger71 handelt es sich um einen Clojure Wrapper um den MongoDB Java Driver. Alle Codebeispiele stammen aus [wwwMongerDocs1], außer wenn anderweitig angegeben. Connect: (ns my.service.server (:require [monger.core :as mg]) (:import [com.mongodb MongoOptions ServerAddress])) ;; localhost, default port (mg/connect!) […] ;; given host, given port (mg/connect! { :host "db.megacorp.internal" :port 7878 }) 71 http://clojuremongodb.info/ 43 Zu beachten ist, dass es eine connect und eine connect! Methode gibt. Der Unterschied ist, dass die connect! Methode die Verbindungsdaten zusätzlich in einer „globalen“ *mongodbconnection* Variablen speichert [wwwMongerAPI]. Festlegen einer Default Database: (ns my.service.server (:require [monger.core :as mg])) ;; localhost, default port (mg/connect!) (mg/set-db! (mg/get-db "monger-test")) Disconnect: (monger.core/disconnect! ) Create Documents: (ns my.service.server (:use [monger.core :only [connect! connect set-db! get-db]] [monger.collection :only [insert insert-batch insert-and-return]]) (:import [org.bson.types ObjectId] [com.mongodb DB WriteConcern])) ;; without document id (when you don't need to use it after storing the document) (insert "document" { :first_name "John" :last_name "Lennon" }) ;; with explicit document id (recommended) (insert "documents" { :_id (ObjectId.) :first_name "John" :last_name "Lennon" }) ;; returns the inserted document that includes generated _id (insert-and-return "documents" {:name "John" :age 30}) ;; multiple documents at once (insert-batch "document" [{ :first_name "John" :last_name "Lennon" } { :first_name "Paul" :last_name "McCartney" }]) ;; with a different database (let [archive-db (get-db "monger-test.archive")] (insert archive-db "documents" { :first_name "John" :last_name "Lennon" } WriteConcern/NORMAL)) 44 Hinweis: (ObjectId.) generiert automatisch eine neue ID für das Dokument. Wird keine ID mit angegeben, so wird diese automatisch vom MongoDB Java Treiber erzeugt, was jedoch bei Clojures unveränderlichen Datenstrukturen nicht funktioniert. Das explizite Angeben einer ID wird deshalb stets empfohlen [wwwMongerDocs1]. Get Document by Id [wwwMongerDocs2]: (let [oid (ObjectId.)] (monger.collection/insert "documents" {:_id oid :first_name "John" :last_name "Lennon"}) (monger.collection/find-map-by-id "documents" oid)) Die fnd-map-by-id Methode gibt ein Dokument aus der Collection „documents“ als Clojure-Map zurück. Update Documents: ;; updates a document by id (monger.collection/update-by-id "scores" oid {:score 1088}) ;; updates score for player "sam" if it exists; creates a new document otherwise (monger.collection/update "scores" {:player "sam"} {:score 1088} :upsert true) Ist die ID bekannt, so kann die Methode update-by-id genutzt werden. Ist die ID nicht bekannt, wird die update Methode benutzt und nach dem Dokument, das :player „sam“ enthält, gesucht. Sofern dieses existiert, wird das Dokument um :score 1088 erweitert. Wird zusätzlich :upsert true übergeben, so wird das Dokument außerdem erzeugt, falls es noch nicht existiert. Remove Documents: ;; remove multiple documents (monger.collection/remove "documents" { :language "English" }) ;; remove ALL documents in the collection (monger.collection/remove "documents") ;; with a different database (let [archive-db (get-db "monger-test.archive")] (monger.collection/remove archive-db "documents" { :readers 0 :pages 0 })) ;; remove document by id (let [oid (ObjectId.)] 45 (monger.collection/insert "documents" { :language "English" :pages 38 :_id oid }) (rmonger.collection/remove-by-id "documents" oid)) Wie man sieht, ist die remove Methode sehr fexibel einsetzbar. Ein Dokument kann anhand seiner ID gelöscht werden, aber genauso auch durch die Angabe eines BeispielDokuments. Zusätzlich können durch das Weglassen weiterer Parameter auch alle Dokumente der Collection gelöscht werden. Die Angabe einer Datenbank kann bei allen Aufrufen jeweils optional erfolgen. Create Collection [wwwMongerDocs3]: ;; creates a non-capped collection (monger.collection/create "recent_events" {}) ;; creates a collection capped at 1000 documents (monger.collection/create "recent_events" {:capped true :max 1000}) Die create Methode kann entweder eine normale Collection erstellen, oder wenn :capped true übergeben wird, eine auf eine bestimmte Anzahl an Dokumenten begrenzte Collection. Zum Löschen von Collections wird die Methode monger.collection/drop verwendet, die den Namen der Collection als einziges Argument erhält. Eine Collection kann außerdem umbenannt werden mit der monger.collection/rename Methode, die den alten und den neuen Namen als Argumente erhält. Querying [wwwMongerDocs4]: Monger bietet zwei Möglichkeiten der Dokumenten-Abfrage. Die Suche anhand eines Beispiel-Dokuments funktioniert ähnlich wie das Querying-by-Example bei ArangoDB. Als Erweiterung gegenüber ArangoDB ist hier jedoch zusätzlich die Nutzung der sogenannten MongoDB Query Operators72 möglich. Diese bieten erweiterte Möglichkeiten, wie etwa die Suche innerhalb von Wertebereichen. Die Ergebnisse können entweder als Cursor oder direkt in Form von Dokumenten als Clojure-Maps zurückgegeben werden. ;; returns a cursor documents with name field value „Ringo“ (monger.collection/find "documents" {:first_name "Ringo"}) ;; with a query that uses MongoDB query operators (monger.collection/find "products" { :price_in_subunits { "$gt" 1200 "$lte" 4000 } }) ;; returns documents with year field value of 1998, as Clojure maps (monger.collection/find-maps "documents" { :year 1998 }) 72 Siehe hierzu: http://docs.mongodb.org/manual/reference/operator/ 46 Darüber hinaus gibt es noch die Methoden monger.collection/fnd-one und monger.collection/ fnd-one-as-map, die jeweils dieselben Aktionen ausführen, aber nur ein Dokument zurückgeben. Letztere Methode bietet zusätzlich die Möglichkeit, einen Vektor mit Attributnamen zu übergeben. In diesem Fall werden vom Ergebnis-Dokument ausschließlich diese Attribute zurückgegeben, was im Falle von großen Dokumenten das unnötige Laden von Daten verhindern kann. Als zweite Möglichkeit der Abfrage wird die Nutzung einer eigenen Abfragesprache, der Monger Query DSL angeboten. Diese sollte benutzt werden, wenn die Ausgabe der Suchergebnisse genauer kontrolliert werden soll. Etwa durch Sortieren, Überspringen von Dokumenten, Einteilung in Seiten (Pagination) und eine Begrenzung der Anzahl der Ergebnisse. Diese Art der Abfrage wird durch eine Aneinanderreihung von Funktionsaufrufen aufgebaut und unterscheidet sich damit grundsätzlich von AQL in ArangoDB, die aus reinem Text besteht. Daher soll die Monger Query DSL hier nicht näher untersucht werden. 4.4.2 Clutch für CouchDB Bei Clutch73 handelt es sich um einen Treiber für die Dokumenten-Datenbank CouchDB. In CouchDB gibt es keine Collections, sondern die Dokumente werden direkt auf der Datenbankebene abgelegt74. Die Ebene der Collections fällt also weg, was die API ein wenig einfacher macht. Codebeispiele aus [Emerick2012], Kapitel 15, und [wwwClutch]. Erstellen einer Datenbank und create Documents: (use '[com.ashafa.clutch :only (create-database with-db put-document get-document delete-document) :as clutch]) (def db (create-database "repl-crud")) ;; [create document] (put-document db {:_id "foo" :some-data "bar"}) ;; [update document] (put-document db (assoc *1 :other-data "quux")) Das erstellte Dokument kann dann mittels (get-document db "foo") abgefragt werden und mit (delete-document db *1) gelöscht werden. Wie man sieht, sind die Methoden in Clutch nach den HTTP-Methoden benannt. Dies signalisiert dem Benutzer, dass darunterliegend 73 http://www.github.com/clojure-clutch/clutch 74 Siehe hierzu auch den Abschnitt „Organization“ in http://openmymind.net/2011/10/27/A-MongoDBGuy-Learns-CouchDB/ 47 eine HTTP-Anfrage stattfindet. Die Erstellung eines CouchDB Dokuments erfolgt mit der Methode clutch/create-document, der eine Clojure-Map übergeben wird. Um mehrere Operationen auf derselben Datenbank durchzuführen, können mittels einer Methode with-db auch mehrere Funktionsaufrufe im Kontext derselben Datenbank ausgeführt werden: (with-db "clutch_example" (put-document {:_id "a" :a 5}) (put-document {:_id "b" :b 6}) (-> (get-document "a") (merge (get-document "b")) (dissoc-meta))) Clutch bietet außerdem noch ein experimentelles Feature: Die Verwendung von Clojureeigenen Collection-Funktionen wie assoc, conj, get etc. zum Arbeiten mit der Datenbank. Dies wurde umgesetzt in Form von Wrapper-Funktionen, die unterliegend auf bereits existierenden Methoden der Clutch API aufbauen. Dieser Ansatz soll im Abschnitt 4.7.3 diskutiert werden. Als wesentlicher Unterschied zur API von Monger befinden sich bei Clutch alle Methoden innerhalb eines Namespaces. Dies macht einerseits den Import leichter, da man sich nicht mit den verschiedenen Namespaces auseinandersetzen muss. Gleichzeitig fehlt aber eine grundsätzliche Gliederung der Methoden. Diese wurde bei Clutch stattdessen im Namen der Methoden vorgenommen, durch ein Anhängen der jeweiligen Ressource, die behandelt werden soll. So etwa bei create-database und create-document. 4.4.3 Elastisch für Elasticsearch Bei Elasticsearch handelt es sich um eine dokumentbasierte Datenbank und eine verteilte Such- und Datenanalyseplattform. Elastisch75 ist ein „minimalistischer“ Clojure Client für Elasticsearch. Codebeispiele aus [wwwElastischDocs1], außer wenn anderweitig angegeben. Create Documents und Connect: (ns clojurewerkz.elastisch.docs.examples (:require [clojurewerkz.elastisch.rest :as esr] [clojurewerkz.elastisch.rest.index :as esi] [clojurewerkz.elastisch.rest.document :as esd])) 75 http://www.github.com/clojurewerkz/elastisch 48 (defn -main [& args] (esr/connect! "http://127.0.0.1:9200" ) ;; submit a document for indexing. Document id will be generated by ElasticSearch, ;; in case the index does not exist, it will be automatically created. (println (esd/create "myapp" "tweet" {:username "happyjoe" :text "My first document submitted to ElasticSearch!" :timestamp "20120802T101232+0100" }))) Als erstes Argument erhält die create Methode den Namen des Indexes („myapp“), unter dem das Dokument abgelegt werden soll. Indexe entsprechen hier einem Namensraum, zu vergleichen mit den Datenbanken in relationalen Systemen [wwwElasticGlossary]. Als zweites Argument wird der sogenannte „mapping type“ angegeben und dann als drittes Argument das abzulegende Dokument. Get Document by Id [wwwElastischDocs2]: (clojurewerkz.elastisch.rest.document /get "myapp" "articles" "521f246bc6d67300f32d2ed60423dec4740e50f5") Update Document: (esd/put "myapp" "tweet" "happyjoe_tweet1" {:username "happyjoe" :text "My first document submitted to ElasticSearch!" :timestamp "20120802T101232+0100" }) Hier wird als drittes Argument der put Methode die document ID übergeben. Create Index: (clojurewerkz.elastisch.rest.index /create "myapp_development") Optional können hier auch die Mapping-Types und weitere Einstellungen mit übergeben werden. Aus diesem kurzen Einblick in die API von Elastisch lässt sich zusammenfassen, dass die Methoden vom Konzept her ähnlich funktionieren wie bei Monger und Clutch. Zudem findet sich auch hier die Methodenbenennung nach HTTP-Verben genau wie bei Clutch. 4.4.4 clj-orient für OrientDB Als Vertreter eines Clients für eine Multimodel-Datenbank soll nun clj-orient 76 für OrientDB untersucht werden. Bei clj-orient handelt es sich um einen Wrapper für die OrientDB Java API. Daher arbeitet der Treiber auch in erhöhtem Maße mit Objekten und 76 http://www.github.com/eduardoejp/clj-orient 49 Klassen. Die Datenbank OrientDB arbeitet ebenso wie ArangoDB mit Collections als übergeordnete Ebene über den Dokumenten, das heißt, bevor man ein Dokument ablegen kann, muss man zunächst eine Collection erstellen [wwwOrientDB1]. Codebeispiele aus [wwwClj-Orient]. Connect: Zum Zwischenspeichern von Verbindungsdaten bietet clj-orient die Möglichkeit, eine Standard-Datenbank mittels set-db! zu setzen. Gleichzeitig kann innerhalb eines geschachtelten Ausdrucks mittels with-db auch eine andere Datenbank verwendet werden: (use 'clj-orient.core) ; Opening the database as a document DB and setting the *db* var for global use. ; A database pool is used, to avoid the overhead of creating a DB object each time. (set-db! (open-document-db! "remote:localhost/my-db" "writer" "writer")) ; Dynamically bind *db* to another DB. ; The DB is closed after all the forms are evaluated. (with-db (open-document-db! "remote:localhost/another-db" "writer" "writer") (form-1 ...) (form-2 ...) (form-3 ...) ... (form-n ...)) ; Close the DB (close-db!) Write Document: (use 'clj-orient.core) (let [u (document :user {:first-name "Foo", :last-name "Bar", :age 10}) u (assoc u :first-name "Mr. Foo", :age 20)] (save! u)) Hier arbeitet clj-orient mit einer save! Methode, wie sie eher aus relationalen Datenbanksystemen bekannt ist. Queries: Es folgen noch zwei Beispiele zum Senden von Queries an die Datenbank. Das erste Beispiel erinnert an das Querying-by-Example von ArangoDB und das zweite Beispiel 50 sendet eine SQL-Anfrage, die zusätzlich noch ein Objekt mit Attributen erhält, die dynamisch in die Anfrage eingesetzt werden. (use 'clj-orient.query) (native-query :user {:country "USA", :age [:$>= 20], :first-name [:$like "J%"]}) (sql-query "SELECT FROM user WHERE country = :country AND age >= :age AND first-name LIKE :fname LIMIT 10" {:country "USA", :age 20, :fname "J%"}) Insgesamt stellt man fest, dass sich clj-orient in der Verwendung etwas anders „anfühlt“ als die bisher untersuchten Treiber Monger, Clutch und Elastisch. Der Treiber ist sehr viel zustandshafter als die bisher untersuchten Treiber (siehe zum Beispiel die Methoden opendocument-db! und save!). Das liegt unter anderem daran, dass clj-orient auf einem Java Treiber aufbaut, in dem intern bei den meisten Aktionen Objekte erzeugt werden. Aus diesen Gründen eignet sich clj-orient weniger als Orientierungshilfe für den Entwurf der Clarango API. 4.4.5 Carmine für Redis Als nächstes soll Carmine77 angeschaut werden. Bei Carmine handelt es sich um einen Client für die Key/Value-Datenbank Redis, der zusätzlich Funktionen einer MessageQueue bietet. Carmine ist ein Projekt, welches versucht, die Vorteile bereits existierender Redis Clients in einem Projekt zu vereinen. Es bietet außerdem Unterstützung für alle Clojure-Datentypen, obwohl Redis intern nur mit Byte-Strings arbeitet. Codebeispiele aus [wwwCarmine]. Connect: (def server1-conn {:pool {<opts>} :spec {<opts>}}) ; See `wcar` docstring for opts (defmacro wcar* [& body] `(car/wcar server1-conn ~@body)) Hier werden per defmacro-Befehl die Verbindungsdaten an den Value wcar* gebunden, sodass diese nicht bei jedem Aufruf von wcar wieder übergeben werden müssen. Read/Write: (wcar* (car/ping) (car/set "foo" "bar") (car/get "foo")) ;; Output: => ["PONG" "OK" "bar"] 77 http://www.github.com/ptaoussanis/carmine 51 Zu beachten ist hier, dass mehrere Befehle gleichzeitig gesendet werden. Diese werden dann auf der Serverseite in einer Pipeline78 abgearbeitet und anschließend alle Ergebnisse zusammen als Vektor zurückgesendet. Zusammenfassend lässt sich sagen, dass Carmine über einen sehr kompakten Befehlssatz verfügt, der sich nur ansatzweise mit dem eines Treibers für eine Dokument-Datenbank vergleichen lässt. Im Unterschied zu den bisher untersuchten Treibern bietet Carmine einen anderen Ansatz für die Verbindungsdatenspeicherung. Hier wird Gebrauch von einem Makro Befehl gemacht, um die Verbindungsdaten dauerhaft an eine Methode zu binden, die für die Verbindung und das Senden der Anfragen zuständig ist. 4.4.6 Neocons für Neo4J Als letztes soll noch ein Treiber für eine Graph-Datenbank untersucht werden: Neocons 79. Es handelt sich hierbei um einen Treiber für die Datenbank Neo4J. Diese bietet genau wie ArangoDB auch eine eigene Query-Sprache: Cypher. Daher soll Neocons auch daraufhin untersucht werden, wie die Anwendung dieser Abfragesprache funktioniert. Codebeispiele aus [wwwNeoconsGuide]. Connect: (neocons.rest/connect! "http://localhost:7474/db/data/") Create Vertices und Edge sowie Ausführen einer Cypher Query: (let [amy (neocons.rest.nodes/create {:username "amy"}) bob (neocons.rest.nodes/create {:username "bob"}) rel (neocons.rest.relationships/create amy bob :friend {:source "college"}) res (neocons.rest.cypher/tquery "START person=node({sid}) MATCH person-[:friend]->friend RETURN friend" {:sid (:id amy)})] (println res))) In diesem Beispiel werden zunächst zwei Knoten „Amy“ und „Bob“ erstellt. Diese werden dann durch eine Kante verbunden. Die anschließend ausgeführte Query gibt alle Freunde von Amy zurück. In diesem Beispiel also nur Bob. Die zum Senden der Query benutzte Methode tquery gibt hierbei das Ergebnis in einer besser lesbaren Tabellenform zurück, während die ebenfalls verfügbare Methode query die Spalten und Zeilen getrennt zurückgibt. 78 Vgl. http://redis.io/topics/pipelining 79 https://github.com/michaelklishin/neocons 52 Graph Traversierungen: Es gibt zwei Arten der Traversierung: die Traversierung von Knoten und die Traversierung von Kanten: (neocons.rest.nodes/traverse (:id john) :relationships [{:direction "out" :type "friend"}] :return-filter {:language "builtin" :name "all_but_start_node"}) (neocons.rest.relationships/traverse (:id john) :relationships [{:direction "out" :type "friend"}]) Man sieht, dass die beiden Arten der Traversierung etwa ähnlich funktionieren. Als erstes Argument wird die ID eines Knoten übergeben, an dem die Traversierung gestartet werden soll. Als zweites Argument wird die Richtung der Traversierung sowie ein Typ von Kanten übergeben, der für die Traversierung berücksichtigt werden soll. Im ersten Beispiel wird außerdem ein Default-Filter eingesetzt, der die Knoten des Ergebnisses filtert. Zusätzlich zu diesen beiden Typen der Traversierung ist außerdem noch eine Traversierung von sogenannten Paths möglich. Bei einem Path handelt es sich um eine Kombination aus Knoten und Kanten. 4.4.7 Zusammenfassung In diesem Abschnitt wurden einige Clojure-Treiber für verschiedene Datenbankmodelle untersucht. Da es sich bei ArangoDB vorwiegend um eine Dokument-Datenbank handelt, eignen sich besonders die untersuchten Treiber für Dokument-Datenbanken als Grundlage für den Entwurf einer API für Clarango. Hier wurde unter anderem ein Einblick gegeben in die Methodenbenennung und -organisation dieser Treiber. Es wurden außerdem Gemeinsamkeiten in der Verbindungsdatenspeicherung bei vielen Treibern festgestellt. Des Weiteren wurden einige Ansätze zur Ausführung von Queries betrachtet und zuletzt noch die Erstellung und Traversierung eines Graphen bei Neocons. Nach der Erläuterung einiger grundsätzlicher Designentscheidungen im nächsten Abschnitt soll im darauffolgenden Abschnitt 4.6 eine API für Clarango hergeleitet werden. Hierfür sollen die Ergebnisse aus der Untersuchung in diesem Abschnitt sowie die Überlegungen aus dem nächsten Abschnitt 4.5 berücksichtigt werden. 53 4.5 grundsätzliche Überlegungen Im Folgenden sollen einige grundsätzliche Überlegungen erläutert werden, die sowohl in das Design der Clarango API, als auch in den Entwurf der Architektur von Clarango mit einbezogen werden. 4.5.1 ähnliche Methoden In der ArangoDB HTTP-API werden oft ähnliche Parameter an verschiedenen Stellen übergeben. Ein gutes Beispiel hierfür ist der Collection-Name. Hier wurden bei den Methoden der ArangoDB API insgesamt drei verschiedene Arten gefunden, diesen zu übergeben. Beim Abfragen und Ändern von Dokumenten beispielsweise ist der Name der Collection Teil des URI eines Dokumentes. Beim Nutzen der by-example Methoden der Simple Queries wird der Collection Name dagegen im JSON-Body des HTTP-Requests übergeben. Beim Anlegen von neuen Dokumenten wird der Collection Name als URLParameter /?collection=... an den URI angehängt. Bei der Clarango API sollen diese Variationen in eine einheitliche Funktionsschnittstelle überführt werden. Alle Methoden der Clarango API sollen möglichst gleich aufgebaut sein in Bezug auf die Reihenfolge der Aufrufparameter. 4.5.2 Angabe von Verbindungsdaten Da das HTTP-Protokoll ein zustandsloses Protokoll ist und die REST/HTTP-API von ArangoDB ebenfalls zustandslos ist, kommt das klassische Herstellen einer DatenbankVerbindung, wie man es von relationalen Datenbanken kennt, bei ArangoDB nicht vor. Sofern der Datenbank-Server erreichbar ist, können Anfragen an diesen gesendet werden und Operationen durchgeführt werden, ohne dass zunächst eine Datenbank-Verbindung hergestellt werden muss. Trotzdem soll es in Clarango eine Möglichkeit geben, dauerhaft Verbindungsdaten wie eine Server-URL und eine Standard-Datenbank zu hinterlegen, damit diese nicht bei jeder Anfrage erneut angegeben werden müssen. Dies soll erreicht werden durch eine Variable im Namespace core. In dieser sollen die Verbindungsdaten als Map abgespeichert werden. Bei der Variablen handelt es sich dann um den einzigen Zustand innerhalb von Clarango. Diese Variante wurde der Einfachheit halber gewählt. Eine Alternative hierzu, die gänzlich auf Seiteneffekte verzichtet, wäre es, die Verbindungsdaten immer in jedem Methodenaufruf zu übergeben. Damit der Benutzer die Daten nicht permanent zwischenspeichern und immer wieder angeben muss, bietet sich bei Clojure in diesem Fall an, die API 54 Methoden zunächst teilweise nach dem folgenden Muster anzuwenden: (def create-document-with-connection (partial document/create {...connection data...})) Als erster Parameter der document/create Methode werden hier die Verbindungsdaten übergeben und dann die Methode inklusive dieser Daten als neue Variable create-documentwith-connection „eingefroren“. In dieser Variante wären die Verbindungsdaten dauerhaft gespeichert, allerdings auf Ebene der Java Virtual Machine und seiteneffektfrei, da es sich bei mehreren mit verschiedenen Parametern teilweise angewendeten Methoden nicht mehr um dieselbe Methode handelt. Diese Art der Verbindungsdatenspeicherung wäre aber für den Benutzer sehr viel aufwendiger als die in der Implementierung von Clarango gewählte Variante, da er sie für jede benutzte Methode getrennt anwenden müsste und unter Umständen mehrere teilangewendete Methoden zwischenspeichern müsste. Der Treiber Carmine (siehe 4.4.5) löst dieses Problem, indem die Verbindungsdaten per Makro-Befehl an eine Methode gebunden werden, die als Hüllmethode für alle API Methoden dient. Diese Hüllmethode wird jedoch im Kontext des Pipelining bei Redis benötigt und wäre bei Clarango nicht sinnvoll. Sinnvoll für Clarango erscheint dagegen aber zusätzlich die Idee einer Methode with-db, wie sie in clj-orient und Clutch enthalten ist (siehe 4.4). Diese Methode bietet eine „Hülle“ oder auch einen „Scope“, in dem alle Methoden mit der Datenbank arbeiten, welche withdb als Argument übergeben wurde. So können auch mehrere Methoden auf einer anderen als der Default-Datenbank arbeiten, ohne dass in jedem Funktionsaufruf wiederholt diese Datenbank angegeben werden muss. 4.5.3 Überprüfung der Eingabedaten Eine grundsätzliche weitere Frage, die sich beim Entwurf von Clarango stellte, war, in wie weit und ob überhaupt die Eingabedaten der Clarango API Methoden auf Gültigkeit überprüft werden sollten80. Eine Überprüfung der Eingabedaten wäre einerseits gut, denn so würden keine ungültigen Anfragen an den Server gesendet. Es würde somit Netzwerk-Trafc gespart. Es könnte hier jeweils überprüft werden, ob alle angegebenen Ressourcen wirklich existieren, ob die Namenskonventionen eingehalten wurden und ob alle geforderten Attribute jeweils angegeben sind (zum Beispiel bei der Erstellung von Kanten eines Graphen die „_from“ und „_to“ Knoten). Gegen die Überprüfung der Eingabedaten spricht allerdings, dass der 80 Mit dieser Fragestellung beschäftigt sich auch diese Diskussion von ArangoDB Treiber-Entwicklern: https://github.com/triAGENS/api-implementors/issues/5 55 Kern von Clarango so leichtgewichtig wie nur möglich sein soll. Clarango soll möglichst wenig Overhead erzeugen und die Anfragen möglichst schnell an den Server weiterleiten. Im Speziellen spricht auch gegen die Überprüfung der Eingabedaten, dass alle oben genannten Punkte auch auf dem ArangoDB Server überprüft werden. Sollte eine Namenskonvention nicht eingehalten worden sein, so wirft ArangoDB einen Fehler. Sollte ein Attribut wie zum Beispiel der „_from“ Knoten bei der Erstellung einer Kante fehlen oder der angegebene Knoten nicht existieren, so wird ebenfalls ein Fehler geworfen. Würden die genannten Punkte schon auf der Clientseite überprüft, so würden zwar einige Server-Anfragen eingespart, es würden aber letztendlich auch alle Tests zwei mal durchgeführt. Zudem kann die Prüfung, ob eine Ressource existiert oder nicht, auf der Clientseite nur durch eine gesonderte Anfrage an den Server durchgeführt werden. Damit würde der Overhead in einem nicht akzeptablen Maße steigen. Es wurde deshalb entschieden, bei Clarango gar keine Überprüfung der Eingabedaten vorzunehmen. Hier bleibt jedoch anzumerken, dass einige Überprüfungen trotzdem automatisch und ohne gesonderten Aufwand erfolgen: Durch den Einsatz der JSONLibrary Cheshire wird automatisch die Gültigkeit der übergebenen JSON-Daten überprüft, das heißt ob sie dem JSON-Standard entsprechen oder nicht. Die Existenz zwingend benötigter Attribute wird außerdem sichergestellt, da sie in der Methodensignatur gefordert werden. Sollte eine Methode ohne das betreffende Argument aufgerufen werden, so wird von Clojure ein Fehler geworfen. 4.5.4 Methodenbenennung Bei der Methodenbenennung stellte sich die Frage, ob und bei welchen Methoden ein Rufzeichen am Ende der Methodennamen stehen soll. Es gibt in Clojure eine inofzielle Konvention81, die vorgibt, dass Methoden, die einen Zustand manipulieren, mit einem Rufzeichen versehen werden sollten. In einem Datenbank-Treiber gilt diese Voraussetzung im Prinzip für alle API-Methoden, da diese den Zustand der Datenbank modifizieren (mit der Ausnahme von reinen Leseoperationen). Bei der Methodenbenennung der ClarangoAPI wurde sich daher an anderen Datenbank-Treibern orientiert. Bei den meisten untersuchten Treibern wurden lediglich die Methoden zur Speicherung der Verbindungsdaten mit einem Rufzeichen versehen (zum Beispiel connect! bei Monger). Genauso wurde deshalb auch bei Clarango verfahren. Die Methoden zur Speicherung der Verbindung im core Namespace haben ein Rufzeichen erhalten, die weiteren API-Methoden in den übrigen Namespaces dagegen nicht. 81 Vgl. http://stackoverflow.com/questions/20606249/when-to-use-exclamation-mark-in-clojure-or-lisp 56 4.5.5 Gliederung der Funktionalitäten in eigene Funktionsräume vs. Gliederung der ArangoDB HTTP-API Die HTTP-Schnittstelle von ArangoDB ist in sehr viele sogenannte Interfaces aufgeteilt. Eines ist etwa zuständig für Dokument-Operationen, eines für Collection-Operationen, eines für Graph-Operationen etc. Es stellte sich die Frage, ob diese Gliederung für die Funktionalitäten von Clarango genau so übernommen werden sollten. Es wurde beschlossen, sich zwar grundsätzlich an der Aufteilung der ArangoDB Interfaces zu orientieren; in Einzelfällen jedoch wurden Methoden in andere Bereiche verschoben, wenn dies sinnvoll schien (dazu mehr in Abschnitt 4.6). Des Weiteren wurden einige ArangoDB Interfaces, die nur wenige Funktionen enthalten oder deren Zuständigkeitsbereiche sich überschneiden, in Clarango zu größeren Funktionsbereichen zusammengefasst. So sollte eine klare Gliederung mit nur wenigen Namespaces hergestellt werden (auch dazu mehr im nächsten Abschnitt). 57 4.6 Clarango API Die Untersuchung aus Abschnitt 4.4 und die Überlegungen aus 4.5 werden nun zusammengeführt und eine API für Clarango entwickelt. Für die Gliederung der Methoden wurde ein Ansatz ähnlich dem von Monger gewählt. Die Methoden werden hierbei in Namespaces gegliedert nach dem Muster Hierarchieebene/ Befehl. Die Hierarchieebene entspricht hierbei den Zuständigkeiten „document“, „collection“, „graph“ etc. Im Gegensatz zu Monger wird hier aber eine feinere und nach Meinung des Autors sinnvollere Gliederung angestrebt. So wird es auch einen Namespace document geben, der für alle Aktionen mit Bezug auf Dokumente benutzt wird. Bei Monger dagegen werden alle Dokument-Operationen auf der Collection-Ebene durchgeführt. Von einem globalen Namespace, der alle Methoden enthält wie etwa bei Clutch, wurde abgesehen, da das oben erläuterte System weitaus übersichtlicher scheint. Es müssen dadurch, um alle Funktionsbereiche von Clarango zu nutzen, zwar mehr Namespaces importiert werden; gleichzeitig wird dadurch aber auch das bewusstere Importieren der Methoden gefördert, was die Gefahr eines unbemerkten Überdeckens von anderen Methoden minimiert. Die Clarango API Namespaces heißen core, document, collection, database, query und graph. Diese Namespaces und die darin enthaltenen Methoden, sowie deren Benennung, sollen nun jeweils kurz erläutert werden. Des Weiteren gibt es noch den experimentellen Namespace collection-ops, hierzu siehe Abschnitt 4.7.3. Die vollständige Liste der Clarango APIMethoden inklusive deren Dokumentation findet sich im Anhang im Abschnitt 10.2. 4.6.1 Clarango Core Wie bereits in Abschnitt 4.5.4 erläutert, stellt der core Namespace Methoden zur permanenten Speicherung von Verbindungsdaten zur Verfügung. Die Namen dieser Methoden wurden aus den in 4.5.5 erläuterten Gründen jeweils mit einem Rufzeichen am Ende versehen. Die Haupt-Methode zur Speicherung von Verbindungsdaten ist setconnection! Diese nimmt eine Map mit Verbindungsdaten entgegen und speichert sie in einer dem core Namespace angehörigen Variablen. Bei den in Abschnitt 4.4 untersuchten Treibern wurde eine ähnliche Methode meist connect! genannt. Für Clarango wurde hier jedoch bewusst ein anderer Name gewählt, um beim Benutzer nicht den Eindruck zu erwecken, dass es sich um eine zustandshafte Datenbank-Verbindung handelt. Ähnlich wie bei Monger ist auch ein Aufruf von set-connection! ohne Argumente möglich. In diesem Fall werden Default-Werte für die Verbindung gesetzt (localhost, Port 8529, „_system“ 58 Datenbank). Zusätzlich gibt es noch die Methoden set-connection-url!, set-default-db!, setdefault-collection! und set-default-graph!, die jeweils nur einen Parameter der Verbindung dauerhaft ändern. Statt set-db!, wie die Methode bei mehreren der in 4.4 untersuchten Treiber heißt, wurde die Methode zur Speicherung eines Datenbanknamens in Clarango set-default-db! genannt. Der Name wurde gewählt, um dem Benutzer bewusst zu machen, dass er trotzdem eine andere Datenbank verwenden kann und es sich nur um einen Default-Wert handelt. Zusätzlich wurden Methoden with-connection, with-db, with-collection und with-graph ähnlich wie bei clj-orient und Clutch implementiert. Diese führen jeweils ein lokales Rebinding der globalen connection Variablen bzw. eines Teiles dieser durch. Im „Scope“ dieser Methoden kann dann jeweils mit der jeweiligen geänderten Verbindung gearbeitet werden, ohne diese permanent ändern zu müssen. 4.6.2 Document API Der document Namespace enthält alle Methoden für das Document-CRUD. Hierzu zählen sowohl die Methoden, die ein Dokument anhand seines Keys finden, ausgeben und ändern, als auch die Methoden, die dieselben Aktionen anhand eines Beispiel-Dokumentes durchführen. In der ArangoDB API handelt es sich hier zwar um zwei verschiedene Interfaces (Interface for Documents und Interface for Simple Queries). Es schien jedoch sinnvoll, diese Methoden in einem Namespace zusammenzufassen, da es sich in beiden Fällen um Document-CRUD handelt. Auch bei Monger sind der Key- und der ExampleAnsatz Teil desselben Namespaces. Im Gegensatz zu Monger sollten die Methoden, die mit einem Beispiel-Dokument arbeiten, in Clarango aber deutlicher als solche gekennzeichnet werden. Daher wurden sie „delete-/replace-/update-by-example“ genannt. Bei Monger sind zudem alle Document-CRUD Methoden Teil des collection Namespaces. Es schien jedoch sinnvoll, bei Clarango einen eigenen Namespace nur für die Dokument-Operationen zu schaffen, um diese als eine Einheit von den Collection-Operationen zu trennen. Bei Clarango wurde sich außerdem gegen die Benennung der Document-CRUD Methoden nach den HTTP-Methoden entschieden, so wie es bei Clutch und bei Elastisch der Fall ist. Dieser Ansatz macht zwar durchaus Sinn, um den Benutzer auf die darunter liegenden HTTP-Anfragen hinzuweisen; es wäre jedoch im Falle einer document/get Methode zu einem Namenskonfikt mit der Clojure-Core Methode get gekommen. Im Falle von Methodennamen put und post ist außerdem nicht intuitiv erkennbar, bei welcher Methode es sich um die Erstellung und bei welcher Methode es sich um das Ersetzen eines bereits bestehenden Dokumentes handelt. Es wurden daher die eindeutigeren Namen 59 create, replace-by-key, replace-by-example sowie update-by-key und update-by-example gewählt. Letztere Methode führt einen HTTP PATCH Request durch. 4.6.3 Collection API Im Unterschied zum Treiber Monger wurden im collection Namespace nur die Methoden untergebracht, die der Erstellung und Modifikation von Collections dienen. Bei Monger dagegen sind auch die Document-CRUD Methoden im collection Namespace enthalten. Um eine sauberere Trennung zu erhalten, wurden die beiden Bereiche in Clarango getrennt. Auch die Methode get-all-documents wurde hier untergebracht, obwohl diese in der ArangoDB API Teil des document Interfaces ist. Da die Methode aber die URIs aller Dokumente einer Collection in einer Liste ausgibt und nicht der Ausgabe der Dokumente selber dient, handelt es sich nach Meinung des Autors eher um eine Operation auf Collection-Ebene als um eine Document-CRUD Methode. Die Methode reiht sich eher ein in die get-[...-]info[-...] Methoden des collection Namespaces, die eine ähnliche Ausgabe erzeugen. 4.6.4 Datenbank API Ähnlich wie die get-all-documents Methode im collection Namespace wurden im database Namespace die Methoden get-collection-info-list und get-all-graphs untergebracht. Diese geben jeweils alle Collections und alle Graphen in der Datenbank als Liste zurück und sind nach Meinung des Autors auf der Datenbank-Ebene einzuordnen. In der ArangoDB API sind sie jedoch jeweils Teil des graph und des collection Interfaces. Des Weiteren enthält der database Namespace noch eine Methode create und eine Methode delete zum Erzeugen und Löschen von Datenbanken ähnlich wie der collection und wie der document Namespace. Hier wurden, um die Konsistenz zu wahren und obwohl es sich um verschiedene Namespaces handelt, für die gleichen Aktionen jeweils dieselben Methodennamen gewählt. Die Aktionen beziehen sich aber auf unterschiedliche Ressourcen, die jeweils am Namespace abzulesen sind. 4.6.5 Query API Im query Namespace von Clarango wurden drei Interfaces der ArangoDB HTTP-API vereint: explain, query und cursor. Alle drei Namespaces sind für das Auswerten und Ausführen von Queries bzw. für das Auswerten der Ergebnisse einer Query zuständig. Um die Clarango API möglichst übersichtlich und kompakt zu gestalten, wurden alle in einem query Namespace vereint. 60 Das Senden von Queries wurde zunächst in einer einfachen Text-Variante umgesetzt. Das heißt es wird den query Methoden lediglich ein fertiger Query-String übergeben, ähnlich wie bei Neocons und seinen Cypher Queries. Als Erweiterung wäre später aber noch eine mehr „clojuresque“ Variante denkbar, wie sie bei der Monger Query DSL 82 oder auch bei dem Graph-Abfrage-Framework clj-gremlin83 vorzufinden ist: Hier werden anstatt Strings zu übergeben mehrere Funktionen ineinander geschachtelt und so die Query zusammengesetzt. 4.6.6 Graph API Die Methoden des graph Namespaces orientieren sich von der Namensgebung her weitestgehend an den Methoden aus dem document Namespace. Es gibt alle CRUDMethoden sowohl für Knoten als auch für Kanten: „get-/replace-/update-/delete-vertex“ sowie „get-/replace-/update-/delete-edge“. Außerdem gibt es genau wie bei den Namespaces document, collection und database eine create und eine delete Methode, um eine GraphRessource erstellen und wieder löschen zu können. Weiterhin gibt es eine Methode zum Ausführen von Graph-Traversierungen: execute-traversal; sowie die Methoden get-vertices und get-edges, die jeweils mehrere zusammenhängende Knoten und Kanten zurückgeben und somit auch eine Art Traversierung darstellen. Hier kann man Parallelen ziehen zum Treiber Neocons, der ebenfalls Methoden bietet, um jeweils nur Knoten oder Kanten zu traversieren oder beides zusammen (was bei Neocons dann „Paths“ genannt wird). Im Falle von graph wurden wieder zwei ArangoDB Interfaces kombiniert: Das eigentliche Graph-Interface und das Interface für Traversierungen. Letzteres enthält nur eine einzelne Methode zum Ausführen von Traversierungen und wurde daher in den graph Namespace integriert. 4.6.7 Flexible Funktionssignaturen Für die Clarango API sollte eine Möglichkeit gefunden werden, die es erlaubt, die APIMethoden in einer möglichst fexiblen Art und Weise aufzurufen. Dies bezieht sich sowohl auf die Möglichkeit, automatisch Default-Werte zu nutzen, zum Beispiel für eine zu verwendende Datenbank, aber auch auf die Möglichkeit, einer Methode zusätzliche Optionen zu übergeben. Das Ziel dabei ist, es möglich zu machen, alle Funktionen in einer möglichst kurzen Art mit wenigen Argumenten aufzurufen. Gleichzeitig sollen bei Bedarf aber auch mehr Argumente erlaubt werden, falls der Benutzer eine detailliertere Kontrolle wünscht. 82 Siehe http://clojuremongodb.info/articles/querying.html 83 https://github.com/olabini/clj-gremlin 61 Sollten die Angaben für die zu verwendende Datenbank und Collection im Methodenaufruf weggelassen werden, so werden automatisch die im core Namespace hinterlegten Default-Werte verwendet. Sollten diese nicht vom Benutzer gesetzt sein, so wird ein Fehler geworfen. Eine ähnliche Möglichkeit, optional eine Datenbank anzugeben, bietet zum Beispiel die Methode insert bei Monger (siehe 4.4.1). Die Möglichkeit zusätzliche Optionen zu übergeben (zum Beispiel um mit der Methode document/updateby-key ein konditionales Update anhand einer Revisionsnummer durchzuführen) wurde umgesetzt, indem den API-Methoden eine Map mit Optionen als zusätzliches Argument übergeben werden kann. Diese kann an einer beliebigen Position zwischen dem CollectionNamen und dem Datenbank-Namen übergeben werden. Es handelt sich jedoch um ein optionales Argument, welches auch weggelassen werden kann. Alle zuletzt erwähnten Parameter stehen jeweils am Ende der Methodensignatur. Die Flexibilität wurde hier erreicht, indem von der Möglichkeit optionaler Parameter bei Clojures Funktionen Gebrauch gemacht wurde. Hierbei kann der Aufrufer einer Methode eine beliebige Anzahl an zusätzlichen Parametern übergeben. Diese werden dann in der Methodenimplementierung als Vektor bereitgestellt. Um die Möglichkeit umzusetzen, eine zusätzliche Map mit Optionen an beliebiger Stelle zu übergeben, wurden zwei Methoden implementiert, die eine Map anhand ihres Typs aus diesem Vektor herausfiltern. 62 4.7 Implementierungsdetails Im Folgenden sollen einige weitere Implementierungsdetails von Clarango erläutert werden, die vom Autor als wichtig angesehen werden. 4.7.1 Error-Handling Zur Behandlung von Fehlern wurde für die erste Implementierung von Clarango eine sehr einfache Variante gewählt. In der Methode, die für das Senden aller HTTP-Anfragen zuständig ist84, wurde ein Try-Catch-Block eingefügt. In diesem findet die gesamte ServerInteraktion statt. Der Block fängt dann alle Fehler ab, die von clj-http geworfen werden. Hierbei handelt es sich entweder um Fehler, die auf dem ArangoDB Server auftreten oder um Fehler, die auf den HTTP/TCP/IP-Schichten auftreten. Ersteres kann zum Beispiel auftreten, wenn versucht wird, eine ungültige Aktion auf dem Server durchzuführen, und letzteres beispielsweise, wenn die Verbindung zum Server fehlschlägt. Im Falle eines Fehlers wird eine weitere Methode zur Behandlung des Fehlers aufgerufen 85. Bei dieser handelt es sich um eine Clojure-Multimethode. Die Methode filtert zwei besondere Fehlerfälle heraus, indem nach Fehlerklassen unterschieden wird. Wenn ein Fehler vom ArangoDB Server kommt, so wird dies als Textmeldung ausgegeben und zusätzlich die Fehlerinformationen von ArangoDB ausgegeben. Im Falle eines Verbindungsfehlers (Server nicht erreichbar oder ähnliches), wird dies ebenfalls als gesonderte Meldung ausgegeben. Für alle anderen Fälle wird in einer DefaultImplementierung der Multimethode lediglich der Fehler geworfen, der vorher abgefangen wurde. 4.7.2 Rückgabewerte Bei den Rückgabewerten der Clarango API-Methoden stellte sich die Frage, was jeweils als Rückgabewert zurückgegeben werden soll. Bei der Abfrage nach einem Dokument ist diese Frage einfach zu beantworten, denn dort soll offensichtlich das Dokument an den Aufrufer der Methode zurückgegeben werden. Bei anderen Operationen wie dem Anlegen oder Löschen von Collections oder Datenbanken ist diese Frage jedoch nicht so leicht zu beantworten. Der ArangoDB Server liefert generell als Antwort auf jede Aktion eine Map mit diversen Angaben zurück. Hierzu gehören oft Angaben über Erfolg und Misserfolg, der HTTP84 die Methode send-request im Namespace http-utility 85 handle-error im selben Namespace 63 Statuscode sowie bei Abfragen das Ergebnis der Abfrage. Dieses Ergebnis ist jedoch oft in der Antwort-Map des Servers verschachtelt und mit unterschiedlichen Keys versehen. Man könnte nun in Clarango als Rückgabewert der Methoden immer genau die Map zurückgeben, die vom Server zurückkommt. Dies wäre konsistent und außerdem sicher gegen Änderungen der ArangoDB API. Dagegen spricht aber, dass der Benutzer bei der Abfrage nach einem Dokument intuitiv erwarten wird, dass er nur das Dokument als Rückgabewert erhält. Genauso wird er bei einer Abfrage nach einer Liste von allen Collections in einer Datenbank auch eine Liste als Rückgabewert erwarten und keine verschachtelte Map, die irgendwo eine Liste enthält. Es wurde deshalb ein Mittelweg gewählt. Der Mittelweg besteht darin, bei offensichtlich erwarteten Rückgabewerten wie oben beschrieben, diese Werte aus dem Server-Ergebnis herauszufiltern und zum Rückgabewert der Methode zu machen. Damit aber bei Bedarf auch alle Daten verfügbar sind, wird das gesamte Server-Ergebnis noch zusätzlich als Clojure-Metadaten an den Rückgabewert angehängt. Ein paar Beispiele: Bei einer Abfrage nach einem Dokument wird das Dokument vom ArangoDB Server direkt unter dem Key :body im Ergebnis zurückgegeben. Bei einer Abfrage nach allen Dokumenten in einer Collection wird das Ergebnis der Abfrage dagegen unter dem Key „documents“ im :body-Teil abgelegt. Bei API-Aufrufen wie der Erzeugung oder Löschung von Ressourcen wird als Haupt-Ergebnis nur ein Boolean-Wert zurückgegeben, der über Erfolg oder Misserfolg berichtet. Dieser wird dann unter dem Key „result“ abgelegt. Die Ergebnisse werden also immer unter verschiedenen Keys abgelegt. Darum wurde eine Methode86 implementiert, die einen Vektor mit Keys übergeben bekommt und einen hierarchischen Keyword-Lookup auf der Map, die vom Server zurückgesendet wird, durchführt. Diese Methode macht es möglich, dass nur das vom Benutzer erwartete Ergebnis direkt von der Clarango API-Methode zurückgegeben werden kann. Damit der Benutzer aber bei Bedarf auch auf alle vom Server zurückgesendeten Informationen zurückgreifen kann, wird die ganze vom Server zurückgelieferte Map immer noch zusätzlich an den Rückgabewert der Clarango API-Methode als Clojure-Metadaten 87 angehängt. 86 incremental-keyword-lookup im Namespace http-utility 87 Siehe http://clojure.org/metadata; Metadaten sind ein Weg, um in Clojure zusätzliche Informationen an einen Wert anzuhängen. 64 4.7.3 „klassische“ Datenbank-Methoden vs. „clojuresque“ Methoden Ein weiterer Aspekt des Designs eines Clojure-Treibers für eine NoSQL Datenbank ist die Überlegung, in wie weit sich die API so entwerfen lässt, dass die Arbeit mit ihr möglichst mit der Clojure-nativen Art der Arbeit auf Datenstrukturen übereinstimmt. Dies wirft insbesondere die Frage auf, in wie weit sich die API Methoden an „klassischen“ DatenbankMethoden orientieren sollen oder ob auf mehr „clojuresque“ Methoden gesetzt werden soll. Einer der Autoren von [Emerick2012] machte den Vorschlag, den CouchDB Treiber Clutch (siehe 4.4.2) um einige Methoden zu erweitern, die den Clojure CollectionMethoden assoc, dissoc, get usw. nachempfunden sind. In [wwwClutchGroup] argumentiert er, dass bei der Benutzung von Clutch 95% der Interaktionen mit der Datenbank eine Benutzung von Clojure-untypischen Methoden verlangen. Die Definitionen der ClojureMengenoperationen wie conj, assoc, dissoc etc. würden aber durchaus auch die Arbeit auf einer Datenbank zulassen. Es gäbe nur den Unterschied, dass diese Methoden, angewendet auf eine Datenbank, deren Zustand verändern, während die Clojure Collection-Methoden immer eine neue Version der jeweiligen Datenstruktur erzeugen. In der Testimplementierung88 hat der Autor die Methodennamen daher mit einem Rufzeichen am Ende versehen, um den Unterschied zu den Clojure-Core-Methoden deutlich zu machen. Die Methoden wurden implementiert, indem auf bereits verfügbare Methoden der Clutch API aufgebaut wurde. Seit Version 0.3.1 sind diese Methoden auch ofziell in Clutch als experimentelles Feature enthalten. In dieser Bachelorarbeit soll der Versuch gemacht werden, ein ähnliches Feature testweise für Clarango umzusetzen. Dabei soll ebenfalls auf die bereits verfügbaren Methoden der Clarango API zurückgegriffen werden. Die erstellten Methoden sollen jedoch nicht zur direkten Modifikation von Datenbanken dienen wie bei Clutch, sondern stattdessen Mengenoperationen auf Collections erlauben. Dies bietet sich an, da es bei ArangoDB im Unterschied zu CouchDB die Collections als Organisationsstruktur für Dokumente gibt. Als Methoden zur probeweisen Implementierung wurden assoc89, dissoc90, conj91 und get92 ausgewählt. Diese sollen in einer Variante für Clarango umgesetzt werden. Um Namenskollisionen mit den gleichnamigen Methoden aus dem Clojure-Core sowie mit den 88 89 90 91 92 Hier einzusehen: https://gist.github.com/cemerick/1485920 http://clojuredocs.org/clojure_core/clojure.core/assoc http://clojuredocs.org/clojure_core/clojure.core/dissoc http://clojuredocs.org/clojure_core/clojure.core/conj http://clojuredocs.org/clojure_core/clojure.core/get 65 Methoden assoc!93, dissoc!94 und conj!95 zu vermeiden, wurden als Namen cla-assoc!, cla-dissoc!, cla-conj! und cla-get! gewählt. Die Methoden wurden in einem separaten Namespace, der als experimentell gekennzeichnet wurde, untergebracht: clarango.collection-ops96. Nach der Wahl dieser Vorgaben war die Umsetzung nicht mehr schwer, da bei der Implementierung auf bereits existierende Methoden aus dem document Namespace zurückgegriffen wurde. Im Unterschied zu den Methoden aus dem document Namespace wird hier jedoch erzwungen, dass das erste Argument der Methode jeweils der Name der Collection sein muss, um eine gleiche Methoden-Signatur wie bei den Clojure-Core Methoden zu erreichen. Des Weiteren stellte sich die Frage, was von den Methoden als Rückgabewerte zurückgegeben werden soll. Die entsprechenden Clojure-Core Methoden geben jeweils eine veränderte Version der Datenstruktur zurück, da der übergebene Wert nicht verändert werden kann. Bei den Clarango Methoden wird jedoch der Wert in der Datenbank verändert und in der Server-Antwort der jeweiligen Operation ist die neue Version der Datenstruktur nicht enthalten. Um trotzdem immer den Inhalt der ganzen Collection als Rückgabewert zurückzugeben, müssten immer mehrere zusätzliche GET-Anfragen an den Server gesendet werden. Dies würde einen großen zusätzlichen Overhead für jeden Methoden-Aufruf bedeuten. Von dieser Möglichkeit wurde daher abgesehen und es werden nur die Server-Rückgabewerte von den Methoden zurückgegeben; bzw. die entsprechenden Rückgabewerte der unterliegenden document Methoden. Als Ergebnis der testweisen Umsetzung dieser Methoden lässt sich daher die Frage stellen, in wie weit diese Collection-Methoden Sinn machen. Sie unterscheiden sich nämlich grundsätzlich in drei wichtigen Punkten von den Clojure-Core Methoden, die ihnen als Vorbild dienen: 93 94 95 96 • sie führen Veränderungen am Zustand der Datenbank durch • sie unterscheiden sich in ihren Rückgabewerten • es wird nicht wie bei den Clojure-Core Methoden das jeweilige Collection-Objekt selbst übergeben, sondern der Name der Collection http://clojuredocs.org/clojure_core/clojure.core/assoc ! http://clojuredocs.org/clojure_core/clojure.core/dissoc ! http://clojuredocs.org/clojure_core/clojure.core/conj ! https://github.com/edlich/clarango/blob/bb365b266ed390a16897a1807999da29b2bd6d55/src/clarango /collection_ops.clj 66 4.7.4 Batch Requests Es wurde ein Ansatz unternommen, mit Clarango auch die von der ArangoDB HTTP-API unterstützten Batch-Requests97 zu unterstützen. Die Verwendung von Batch-Requests bietet sich generell an, wenn wiederholt eine ähnliche Aktion ausgeführt werden soll. Ein Beispiel ist das Einfügen mehrerer Dokumente nacheinander in eine Collection. Beim Senden einzelner HTTP-Requests für jede Anfrage entsteht hier ein vermeidbarer Overhead durch die verwendeten Protokolle HTTP/TCP/IP usw. Dieser lässt sich vermeiden, wenn alle Einfüge-Aktionen gebündelt als ein HTTP-Request gesendet werden, was durch das BatchInterface der ArangoDB API möglich gemacht wird. Das Batch-Interface erwartet hierbei, dass alle Anfragen hintereinander im Textkörper der HTTP-Anfrage gesendet werden. Die Umsetzung mit der Library clj-http stellte sich jedoch als schwer heraus. Die Library bietet zwar die Möglichkeit des Sendens von mehreren POST-Requests innerhalb einer HTTP-Anfrage in Form von „Multipart FormPosts“. Es wurde zunächst angestrebt, diese Möglichkeit auch zu nutzen, um die BatchRequests umzusetzen. Sehr schnell zeichnete sich jedoch ab, dass das von der ArangoDB API erwartete Format des HTTP-Textkörpers mit clj-http nur schwer umzusetzen ist. Letztendlich scheiterte das Vorhaben daran, dass der von ArangoDB erwartete „TrennString“ zur Markierung der einzelnen Teil-Requests vom Server schon im Header der HTTP-Anfrage erwartet wurde. Bei den Multipart Requests in clj-http wird dieser jedoch automatisch generiert und es wurde keine Möglichkeit gefunden, den String bereits bei der Generierung des HTTP-Headers zu erhalten und mitzusenden98. Als Lösung würde sich hier anbieten, direkt auf die von clj-http benutzte Java Library Apache HttpComponents zuzugreifen und dort nach einer Lösung zu suchen. Dies ist jedoch ein erhöhter Aufwand und wurde aus Zeitgründen bisher nicht umgesetzt. 4.7.5 Allgemein verwendbare unterliegende Methoden Bei der Implementierung von Clarango wurde Wert darauf gelegt, dass die Implementierungen der API-Methoden möglichst kurz sind. Hierzu wurde ein Satz an unterliegenden Methoden geschaffen, die von allen Methoden der Clarango API gleichsam in deren Implementierung verwendet werden. Die Implementierungen der API-Methoden sind daher meist nur eine Zeile oder wenig mehr lang. 97 http://www.arangodb.org/manuals/current/HttpBatch.html 98 Dieses Problem wurde auch im clj-http GitHub-Repository als Frage formuliert, bis zum Zeitpunkt der Fertigstellung dieser Arbeit wurde diese jedoch nicht beantwortet: https://github.com/dakrone/cljhttp/issues/191 67 Erreicht wurde dies durch von allen API-Methoden verwendeten Utility-Methoden zum URI-Building, Wrapping der HTTP-Requests, sowie dem Lookup der DefaultVerbindungsdaten. Diese Methoden befinden sich in den Namespaces im Unterordner src/clarango/utilities. Sie wurden so allgemein gehalten, dass sie bei der Implementierung aller API-Methoden eingesetzt werden konnten. Für weitere Details zur Architektur von Clarango und eine ausführlichere Beschreibung der Utility-Methoden siehe auch die nächsten beiden Abschnitte 4.8 „Clarango SystemArchitektur“ und 4.9 „Exemplarische Untersuchung: Aufbau und Aufruf einer Clarango Methode“. 68 4.8 Clarango System-Architektur Im Folgenden soll der Aufbau und die Architektur von Clarango erläutert werden. Hierzu wird jeweils eine kurze Beschreibung aller in Clarango vorhandenen Namespaces gegeben und anschließend ihr Zusammenspiel in einem Diagramm verdeutlicht. 4.8.1 Clarango Namespaces Die Namespaces in Clarango sind in zwei Ebenen gegliedert: 1. Namespaces mit API-Methoden, die vom Benutzer verwendet werden. 2. Utility-Namespaces, die nur intern verwendet werden sollen. Die Datei-Struktur der Namespaces: Abb. 2: Clarango Source Dateistruktur core Enthält Funktionen zur Speicherung von Verbindungsdaten. Abhängigkeiten: keine utilities/core-utility Funktionen zum Abrufen der in core gespeicherten Verbindungsdaten sowie Funktionen zum Filtern von optionalen Funktionsparametern bei den API Methoden. Abhängigkeiten: core utilities/uri-utility Beinhaltet die Funktion zum Zusammenbau der Ressourcen-URIs. Abhängigkeiten: core-utility 69 utilities/http-utility Bietet Funktionen zum Zugriff auf Datenbank-Ressourcen. Dieser Namespace benutzt die clj-http Library zum Senden von HTTP-Anfragen, abstrahiert diese jedoch weiter, um in den Methoden der Clarango-API einen vereinfachten Zugriff auf die Ressourcen zu haben und um Code-Wiederholungen zu vermeiden. Er fängt außerdem Fehler auf, die auf dem HTTP-Level auftreten und wirft Custom-Fehlermeldungen. Abhängigkeiten: core-utility, uri-utility, Cheshire Library, clj-http Library database Beinhaltet die Funktionen der Datenbank-Ebene, wie das Erstellen und Ändern von Datenbanken und den Abruf von Informationen über diese. Abhängigkeiten: core-utility, http-utility, uri-utility collection Beinhaltet die Funktionen der Collection-Ebene, wie das Erstellen und Ändern von Collections und den Abruf von Informationen über diese. Abhängigkeiten: core-utility, http-utility, uri-utility document Beinhaltet alle Funktionen des Document-CRUD. Dazu zählen das Document-CRUD mit bekanntem Document-Key sowie das Document-CRUD mit Angabe eines BeispielDokuments. Abhängigkeiten: core-utility, http-utility, uri-utility graph Beinhaltet alle Funktionen zum Erstellen und Bearbeiten von Graphen, sowie zum Abrufen von Knoten und Kanten und dem Ausführen von Graph-Traversierungen. Abhängigkeiten: core-utility, http-utility, uri-utility query Beinhaltet Funktionen zum Ausführen und Auswerten von AQL-Queries. Außerdem gibt es Funktionen zum Umgang mit Datenbank-Cursors; so kann in einer großen Menge von Suchergebnissen navigiert werden. Abhängigkeiten: core-utility, http-utility, uri-utility collection-ops Dieser Namespace bietet Abstraktionen der Document-CRUD Methoden aus dem Namespace document. Dadurch soll eine mehr „clojuresque“ Art des Arbeitens mit 70 Collections und Dokumenten geboten werden. Siehe dazu auch Abschnitt 4.7.3. Abhängigkeiten: document main Dieser Namespace ist nur im Git-Branch development enthalten, da er nicht Teil der Library Clarango sein soll. Er beinhaltet eine Main-Methode, die eine ausführliche Demonstration der Großzahl aller Clarango API-Methoden bietet. So kann in der Kommandozeile mit dem Befehl „lein run“ ein ausführlicher Test durchgeführt werden99. Abhängigkeiten: alle Namespaces mit Ausnahme der Utility-Namespaces 4.8.2 Diagramm Im folgenden Diagramm ist das Zusammenspiel der Namespaces und der importierten Libraries in Clarango verdeutlicht. Ein Pfeil bedeutet, dass der Namespace, von dem der Pfeil ausgeht, den Namespace, auf den der Pfeil zeigt, importiert. Abbildung 3: Clarango Architektur und Abhängigkeiten der Namespaces 99 Nicht zu verwechseln jedoch mit den eigentlichen Tests, die mit „lein test“ ausgeführt werden können. 71 4.9 Exemplarische Untersuchung: Aufbau und Aufruf einer Clarango Methode Hier soll exemplarisch der Aufbau einer Methode des document Namespaces untersucht werden. Dadurch soll der Aufbau von Clarango, das Zusammenspiel der Namespaces und der Zweck der verschiedenen Utility-Methoden verdeutlicht werden. Das folgende Diagramm verdeutlicht das Zusammenspiel der Namespaces für einen Aufruf einer Methode des document Namespaces. Abbildung 4: Ablaufdiagramm: Aufruf einer Methode aus dem document Namespace (allgemein) 72 Als Beispiel für eine detailliertere Untersuchung soll nun ein Aufruf der Methode document/get-by-example dienen. Die Methode soll mit folgenden Parametern aufgerufen werden: (document/get-by-example {:name "some test document"} {"limit" 2} "test-collection") In dem Beispiel wird ein Dokument als erster Parameter übergeben. Nach diesem soll in allen Dokumenten der Collection „test-collection“ gesucht werden. Das heißt es sollen alle Dokumente zurückgegeben werden, die das Attribut :name „some test document“ enthalten. Außerdem wird eine Map mit dem optionalen Parameter „limit“ 2 übergeben, womit die Anzahl der Suchergebnisse auf zwei begrenzt wird. Da hier nicht explizit eine Datenbank angegeben wurde, soll die im Clarango-Core gespeicherte Default-Datenbank verwendet werden. In diesem Beispiel soll diese den Namen „test-DB“ tragen. Die im Core gespeicherte Server-URL soll „http://localhost:8529“ sein. Ein Blick in die Datei document.clj100 offenbart die relativ kurze Implementierung der getby-example Methode: (defn get-by-example "[… docstring …]" [example & args] (http/put-uri [:body "result"] (build-ressource-uri "simple/by-example" nil nil (filter-out-database-name args)) (merge {:example example :collection (filter-out-collection-name args)} (filter-out-map args)))) Die Liste der Methoden-Argumente besteht nur aus einem festen Argument: Der ExampleMap, nach der gesucht werden soll (example). Alle weiteren Argumente sind optional (& args). Hierzu zählen der Collection-Name, der Datenbank-Name und eine Map mit weiteren Optionen (im obigen Beispiel {„limit“ 2}). Sollten beim Aufruf kein CollectionName und/oder kein Datenbank-Name angegeben werden, so wird jeweils der DefaultWert verwendet, welcher im Core gesetzt wurde. Soll jedoch ein Datenbank-Name angegeben werden, so muss auch ein Collection-Name angegeben werden, da in der Parameter-Liste der Collection-Name vor dem Datenbank-Namen steht. Im Folgenden sollen kurz alle wichtigen Methoden, die in der get-by-example Methode aufgerufen werden (rötlich hervorgehoben), erläutert werden. filter-out- Methoden Diese Methoden aus dem Namespace core-utility dienen dazu, aus dem Vektor der optionalen Funktionsparameter (& args) von get-by-example die jeweils benötigten Teile 100https://github.com/edlich/clarango/blob/master/src/clarango/document.clj 73 herauszufiltern. Es wird erkannt, an welcher Stelle sich die Map mit weiteren Optionen, falls vorhanden, befindet, und an welchen Stellen sich, falls vorhanden, der Name einer Collection und einer Datenbank befinden. Sollten letztere bei einem Aufruf von get-byexample nicht vorhanden sein, so werden automatisch die Default-Werte aus dem Clarango Core nachgeschaut und zurückgegeben. Für den oben gegebenen Beispiel-Aufruf der get-by-example Methode gibt flter-out-map die Options-Map {„limit“ 2} zurück. flter-out-collection-name gibt den Namen „test-collection“ zurück und flter-out-database-name gibt den Namen der im Clarango Core gespeicherten Default-Datenbank „test-DB“ zurück. build-ressource-uri Mit einem Aufruf von uri-utility/build-ressource-uri wird der URI der Ressource, auf die zugegriffen werden soll, zusammengesetzt. Der Quellcode der Methode lautet: (defn build-ressource-uri "[… docstring …]" ([type] (connect-url-parts (get-safe-connection-url) "_api/" type)) ([type ressource-key] (connect-url-parts (get-safe-connection-url) "_db/" (get-default-db) "_api/" type (get-default-collection-or-graph type) ressource-key)) ([type ressource-key collection-name] (connect-url-parts (get-safe-connection-url) "_db/" (get-default-db) "_api/" type collection-name ressource-key)) ([type ressource-key collection-name db-name] (connect-url-parts (get-safe-connection-url) "_db/" db-name "_api/" type collection-name ressource-key))) Es handelt sich um eine Methode mit verschiedenen Implementierungen für jeweils eine unterschiedliche Anzahl an Argumenten. In jedem Fall gefordert ist der type Parameter. Hierbei handelt es sich im Allgemeinen um den Namen des Interfaces der ArangoDB HTTP-API, auf das zugegriffen werden soll. Zum Beispiel „document“ oder „graph“. Als zweiter Parameter kann der Key einer Ressource übergeben werden. Hier wird üblicherweise der Key eines Dokumentes übergeben. Oft wird hier aber auch nil übergeben, wenn ein URI ohne Key gebildet werden soll. Dies ist auch bei der Methode get-by-example der Fall, da hier nach Dokumenten gesucht werden soll, deren Keys noch unbekannt sind. Für das oben genannte Beispiel wird die build-ressource-uri Methode mit vier Parametern aufgerufen. Für den ersten Parameter type wird der String „simple/by-example“ übergeben; 74 „simple“, da es sich um das API-Interface der Simple-Queries101 handelt; „by-example“ ist die API-Methode, auf die zugegriffen werden soll. Als Ressource-Key wird nil übergeben (da die Keys noch unbekannt sind) und ebenso auch als Collection-Name, da dieser bei der byexample Methode im Body der HTTP-Anfrage übergeben werden muss. Als vierter Parameter wird der von der Methode flter-out-collection-name zurückgegebene Name der Default-Datenbank „test-DB“ übergeben. Der Rückgabewert der build-ressource-uri Methode lautet somit: http://localhost:8529/_db/test-DB/_api/simple/by-example put-uri Auf der äußersten Ebene der Methodenimplementierung von get-by-example steht ein Aufruf der Methode http/put-uri. Diese sendet eine PUT-Anfrage an die Ressource, deren URI vorher von der build-ressource-uri zusammengesetzt wurde. Die Implementierung der Methode im Namespace http-utility lautet: (defn put-uri ([response-keys uri] (send-request :put response-keys uri nil nil)) ([response-keys uri body] (send-request :put response-keys uri body nil)) ([response-keys uri body params] (send-request :put response-keys uri body params))) Hierbei handelt es sich nur um eine Hüllmethode für die Methode, die die eigentlichen HTTP-Anfragen sendet: send-request. Die Hüllmethode put-uri wurde geschaffen, damit sich send-request auf einfache Art mit einer verschiedenen Anzahl an Parametern aufrufen lässt. In den Implementierungen der Clarango API-Methoden reduziert sich so die Anzahl der nils, die übergeben werden müssen wenn einzelne Parameter weggelassen werden sollen. Auch die Auswahl der HTTP-Methode wird hier gleich mit vorgenommen: :put. Die Implementierung der send-request Methode lautet: (defn- send-request [method response-keys uri body params] (if (console-output-activated?) (println (get-uppercase-string-for-http-method method) " connection address: " uri)) (try (let [map-with-body (if (nil? body) {} {:body (generate-string body)}) response (http/request (merge {:method method :url uri :debug (httpdebugging-activated?) :query-params params} map-with-body)) filtered-response (filter-response response response-keys)] (if (type-output-activated?) (println (type filtered-response))) 101http://www.arangodb.org/manuals/1.4.5/HttpSimple.html 75 ;; append the original server response (filtered only one level, usually :body) as metadata (with-meta filtered-response (filter-response response [(first response-keys)]))) (catch Exception e (handle-error e)))) Die Methode beginnt mit einer Konsolen-Ausgabe, bei der der URI der Ressource und die HTTP-Methode ausgegeben werden, sofern ein globaler Switch im Namespace (consoleoutput-activated?) auf true gesetzt wurde. Danach beginnt die eigentliche Implementierung der Methode. Diese ist vollständig in einen try-catch Block gehüllt. Hier werden Verbindungsfehler und Fehler, die vom Datenbank-Server kommen, abgefangen. Zur Behandlung der Fehler wurde eine separate Methode handle-error geschaffen. Bei dieser handelt es sich um eine Clojure-Multimethode, die verschiedene Fehlerarten unterschiedlich behandelt. Innerhalb des try Blocks findet das eigentliche Senden der Server-Anfrage statt. Hierzu wird Gebrauch von der request Methode aus der clj-http Library gemacht. Diese Methode unterstützt alle HTTP-Verben. Aus dem Rückgabewert der request Methode werden dann die gewünschten Teile herausgefiltert, die der send-request Methode im Parameter responsekeys als Vektor übergeben wurden. Im Falle von get-by-example lautet dieser Vektor [:body „result“]. Das bedeutet, dass aus dem Server-Rückgabewert zunächst der Teil herausgefiltert wird, der unter dem Key :body zu finden ist, und dann aus diesem der Teil, der unter dem Key „result“ zu finden ist. Der ungefilterte Rückgabewert der Server-Anfrage wird dann noch zusätzlich an den Rückgabewert von send-request mittels with-meta als Clojure Metadaten angehängt. Zusammenfassung Im oben genannten Beispiel wird die HTTP-Anfrage an den folgenden URI gesendet: http://localhost:8529/_db/test-DB/_api/simple/by-example Der Body der HTTP-Anfrage enthält das Beispiel-Dokument :example {:name „some test document“} und den Namen der Collection :collection „test-collection“ sowie den optionalen Parameter „limit“ 2 Durch die Verwendung der Cheshire Library werden alle Parameter vor dem Senden in das JSON-Format gebracht, wobei unter anderem alle Clojure Keywords in einfache Strings umgewandelt werden. 76 Zur Verdeutlichung des Ablaufs wird dieser nochmals in einem Diagramm dargestellt. Hier sind nun zusätzlich die aufgerufenen Methoden und, falls nicht zu lang, deren Rückgabewerte dargestellt: Abbildung 5: Ablaufdiagramm: Aufruf einer Methode aus dem document Namespace (speziell) 77 5. Testing / Qualitätssicherung Um die ordnungsgemäße Funktion von Clarango sicherzustellen, wurde damit begonnen, eine Reihe von Tests für die Library zu implementieren. Diese können in Leiningen dann automatisch mit dem Kommando „lein test“ ausgeführt werden. Der Test-Code ist auf GitHub hier einsehbar: https://github.com/edlich/clarango/tree/master/test/clarango/test Für jeden Namespace der Clarango Library soll eine Datei mit Testcode erstellt werden. Diese enthält dann jeweils einen Aufruf des Makros deftest, welches eine „Test-Suite“ für den jeweiligen Namespace definiert. Bei der „Suite“ handelt es sich um eine Funktion ohne Argumente und mit entsprechenden Metadaten, die sie als Test kennzeichnen. Diese Funktion enthält wiederum Aufrufe der Methoden der Clarango API. Es wird dann jeweils überprüft, ob die API Methoden die erwarteten Ergebnisse zurückliefern bzw. ob die erwarteten Fehler auftreten. Alle in den Tests verwendeten Funktionen und Makros stammen aus dem standardmäßig in Clojure integrierten Test-Framework clojure.test102. Zum Zeitpunkt der Fertigstellung dieser Arbeit wurden die drei Test-Suites core-test, database-test und document-test erstellt. Hier soll einmal beispielhaft der Code für den Test des core Namespaces abgebildet werden: (ns clarango.test.core (:use clojure.test clarango.core)) (deftest core-test (testing "Check the correct connection settings" (is (nil? (get-connection)) "con must be nil initially") (is (false? (connection-set?)) "con must be nil initially") (set-connection!) ;; call without arguments (is (= {:db-name "_system", :connection-url "http://localhost:8529/"} (get-connection)) "Mandatory default values!") (set-connection-url! "http://localhost:9999/") (set-default-db! "another-db") (set-default-collection! "another-collection") (is (= {:db-name "another-db", :connection-url "http://localhost:9999/", :collection-name "another-collection"} (get-connection)) "obvious receive what has been set"))) 102http://clojuredocs.org/clojure_core/clojure.test 78 Zusätzlich zur Möglichkeit des lokalen Ausführens der Tests mit Leiningen wurde auch von der Möglichkeit Gebrauch gemacht, den Build- und Testing-Dienst Travis CI103 in das Clarango GitHub-Repository einzubinden. Der Dienst wird unter anderem durch das Einfügen einer Datei .travis.yml104 in das Repository aktiviert. Bei jedem Commit, der auf GitHub hochgeladen wird, werden dann alle definierten Tests ausgeführt [wwwTravisGuide]. Sollte dies ohne Fehler erfolgen, so wird im GitHub-Repository ein grüner Button mit der Aufschrift „build passing“ angezeigt. Über die Konfigurationsdatei .travis.yml wurde außerdem ein Installationsscript für ArangoDB in das Repository eingebunden105, sodass in der Travis CI-Testumgebung eine lauffähige Version von ArangoDB zur Verfügung steht. 103https://travis-ci.org/ 104https://github.com/edlich/clarango/blob/master/.travis.yml 105Hier einsehbar: https://github.com/edlich/clarango/blob/df75ac49d5b0c527b668ef75c135c914fa64c95b/setup_arango db_1.4.sh 79 6. Anwendungsdemo Im Folgenden soll ein Beispiel zur Benutzung der Clarango Library gegeben werden. Dabei sollen möglichst alle Methoden der Clarango API abgedeckt werden. In Fällen, wo sich API-Methoden sehr ähnlich sind, wurde aber unter Umständen auf den zusätzlichen Aufruf einer weiteren Methode verzichtet. Außerdem sollen die verschiedenen Arten der Angabe von Verbindungsdaten demonstriert werden. Das hier abgebildete Codebeispiel wird abschnittweise abgedruckt und kommentiert und ist in ähnlicher Form auch im Branch development im Namespace main zu finden: https://github.com/edlich/clarango/blob/8829d7f351a8679b112c28ffdd6a9fb6ff54dcf5/src /clarango/main.clj 1. Erstelle eine Datenbank, sowie eine Collection und führe einige Document-CRUD Operationen durch: ;; connect to defaults: localhost and port 8529 (cla-core/set-connection!) ;; create Database "test-DB" (database/create "test-DB" [{:username "test-user"}]) ;; create Collection "test-collection" in DB "test-DB" (collection/create "test-collection" "test-DB") ;; document CRUD (document/create {:_key "test-doc" :name "some test document"} "test-collection" "test-DB") (document/update-by-key {:additional "some additional info"} "test-doc" "testcollection" "test-DB") (document/get-by-key "test-doc" "test-collection" "test-DB") (document/replace-by-example {:name "new version of our test document"} {:additional "some additional info"} "test-collection" "test-DB") 2. Benutze die „clojuresquen“ Methoden aus dem Namespace collection-ops, um weitere Dokumente zur bereits existierenden Collection hinzuzufügen, zu lesen und wieder zu löschen: ;; set default DB; this database will be used in the following methods without explicitely having to pass it (cla-core/set-default-db! "test-DB") ;; collection ops : assoc, dissoc, conj (cla-assoc! "test-collection" "new-document-1" {:description "some test document to test the clojure idiomatic collection methods" :key-type "given key"}) (cla-conj! "test-collection" {:description "some test document to test the clojure idiomatic collection methods" :key-type "auto generated key"}) (cla-get! "test-collection" "new-document-1") (cla-dissoc! "test-collection" "new-document-1") 80 3. Nehme Änderungen an der Collection vor und gebe einige ihrer Eigenschaften aus. Entferne sie zuletzt aus dem Arbeitsspeicher des Datenbank-Servers und lösche sie dann ganz: ;; get information about the collection and a list of all documents inside it (collection/get-info "test-collection") (collection/get-all-documents "test-collection") ;; rename the collection and modify it's properties (collection/rename "new-name-test-collection" "test-collection") (collection/modify-properties {"waitForSync" true} "new-name-test-collection") (collection/get-extended-info-figures "new-name-test-collection") ;; unload and delete collection (collection/unload "new-name-test-collection") (collection/delete "new-name-test-collection") 4. Erstelle einen Graphen sowie die dazu notwendigen Collections für Knoten und Kanten. Führe einige Graph-Operationen wie das Erstellen, Löschen und Ausgeben von Knoten durch. Führe eine Graph-Traversierung durch. Suche weiterhin in der Vertex-Collection mittels einer AQL-Abfrage nach Knoten mit bestimmten Eigenschaften. Gebe außerdem alle verfügbaren Datenbanken aus, sowie alle Collections in der gerade neu erstellten Datenbank. Benutze die with-db und with-graph Methoden zum Festlegen eines Kontextes für die weiteren Operationen. Lösche zuletzt alle Datenbanken: ;; first create another Database "GraphTestDB" (database/create "GraphTestDB" []) ;; now list all available databases (database/get-info-list) ;; perform next operations in the context of "GraphTestDB" (with-db "GraphTestDB" ;; create vertex and edge collections "people" and "connections" (collection/create "people" {"type" 2}) (collection/create "connections" {"type" 3}) ;; now list all available collections, excluding the system collections (database/get-collection-info-list {"excludeSystem" true}) ;; create graph "test-graph" (graph/create "test-graph" "people" "connections") ;; now get all available graphs (database/get-all-graphs) ;; perform next operations in the context of the graph "test-graph" (with-graph "test-graph" ;; create vertices "Peter", "Bob", "Clara", "Jessica", "Alice" with :ages (graph/create-vertex {:_key "peter" :name "Peter" :age 25}) (graph/create-vertex {:_key "bob" :name "Bob" :age 28}) (graph/create-vertex {:_key "clara" :name "Clara" :age 29}) (graph/create-vertex {:_key "jessica" :name "Jessica" :age 23}) (graph/create-vertex {:_key "alice" :name "Alice" :age 20}) 81 ;;; perform query: find all people who are older than 24 ;; first validate the query ;; then explain (how the query would be executed on the server) ;; then actually execute it (query/validate "FOR p IN people FILTER p.age > 24 RETURN p") (query/explain "FOR p IN people FILTER p.age > 24 RETURN p") (query/execute "FOR p IN people FILTER p.age > 24 RETURN p") ;; create edges with labels "friend", "boyfriend", "girlfriend" ;; save one key to use this edge later (let [edge-key (get (graph/create-edge {:$label "friend"} "peter" "alice") "_key")] (graph/create-edge {:$label "friend"} "alice" "clara") (graph/create-edge {:$label "friend"} "clara" "jessica") (graph/create-edge {:$label "boyfriend"} "alice" "bob") (graph/create-edge {:$label "girlfriend"} "bob" "alice") ;; get vertices that have connections going from the vertex "peter" (graph/get-vertices "peter" 10 10 true nil) ;; update one edge (graph/update-edge {:description "Peter and Alice have been friends for over 6 years"} edge-key) ;; get all edges that are outgoing from the vertex "peter" (graph/get-edges "peter" 10 10 true nil) ;; execute a graph traversal (graph/execute-traversal "peter" "people" "connections" "inbound") ;; delete one edge (graph/delete-edge edge-key) ;; delete one vertex (graph/delete-vertex "peter"))) ;; delete the graph (graph/delete "test-graph")) ;; delete databases (database/delete "GraphTestDB") (database/delete "test-DB") Im Anhang unter 10.1 finden sich zusätzlich die Ergebnisse des hier gezeigten Beispiels in Form von Konsolenausgaben. 82 7. Fazit und Ausblick Im Verlaufe dieser Arbeit wurde eine funktionierende Version eines Clojure-Treibers für ArangoDB entwickelt. Dabei wurden zwar nicht alle Funktionen, die das ArangoDB HTTP-Interface bietet, in Clarango umgesetzt. Es wurde jedoch ein Großteil der Funktionen implementiert, die ArangoDB als Multimodel-Datenbank charakterisieren: Document-CRUD und Graph-Funktionen; außerdem weitere Funktionen, die im Rahmen dessen benötigt werden, wie das Management von Collections und Datenbanken und die Verbindungsdatenspeicherung; außerdem wurden Query-Funktionalitäten implementiert. Eine genaue Checkliste der in Clarango implementierten ArangoDB Interfaces findet sich im Anhang unter 10.3. Beim Design des Treibers wurde versucht, eine möglichst konsistente und intuitive API zu entwerfen. Um dies zur erreichen, wurden verschiedene Clojure-Treiber für andere NoSQL Datenbanken untersucht. Beim Design der Clarango API wurden dann Eigenschaften, die bei mehreren Treibern übereinstimmten, sowie Ideen, die sinnvoll erschienen, übernommen. Es konnte aber nur eine begrenzte Anzahl an Treibern untersucht werden und auch nicht alle Aspekte von allen Treibern konnten gleichzeitig mit in die Clarango API einfießen. Bei der Auseinandersetzung mit der ArangoDB HTTP-API wurde festgestellt, dass diese nicht einhundertprozentig konsistent ist. Am Beispiel der Collection-Namen hat man gesehen, dass diese bei verschiedenen API-Methoden an ganz unterschiedlichen Stellen übergeben werden. Dies hat vermutlich den Grund, dass die API zusammen mit ArangoDB über den Zeitraum von mehr als einem Jahr gewachsen ist. So wurden andere Vorgehensweisen bei neu hinzugefügten API-Methoden vermutlich für besser befunden als die bereits existierenden. Um die Abwärtskompatibilität zu wahren, wurden jedoch die älteren, schon bestehenden Methoden nicht mehr geändert. Hier zeigt sich, wie schwer es ist, eine konsistente API zu entwerfen. Auch bei sorgfältiger Planung am Anfang werden sich im Laufe der Weiterentwicklung einer Software die Anforderungen ändern oder auch die Ansichten der Entwickler darüber, was gutes und was schlechtes Design ist. Zusätzlich wird es, je größer eine API wird, immer schwerer, einen konsistenten Stil zu wahren. Da Clarango vollständig innerhalb von ein paar Monaten umgesetzt wurde, wurde bisher gut das Ziel erreicht, eine konsistente API zu schaffen. Im Laufe der weiteren Entwicklung von Clarango wird dies allerdings vermutlich immer schwerer werden, besonders wenn sich die API von ArangoDB einmal ändern sollte. 83 Es wurde weiterhin angestrebt, beim Design der Clarango API ein möglichst „clojuresques“ Design umzusetzen. Hier wurde versucht, sich an den Standards und Best-Practises der Sprache zu orientieren. Es zeigte sich deutlich das Spannungsfeld zwischen einer zustandshaften Datenbank auf der einen Seite und dem zustandslosen Ansatz von REST und Clojure auf der anderen Seite. Diese beiden „Pole“ sollten so gut es geht miteinander vereinbart werden. Hier musste abgewogen werden zwischen vollständiger Zustandslosigkeit bzw. funktionalem Design und der Zwischenspeicherung von Verbindungsdaten. Letztere erhöht nach Meinung des Autors erheblich den Benutzungskomfort von Clarango. Daher wurde ein Kompromiss eingegangen und eine Variable eingeführt, die Verbindungsdaten zwischenspeichert und die mit verschiedenen Methoden manipulierbar ist. Trotzdem lässt sich wohl sagen, dass Clarango gegenüber „klassischen“ Datenbanktreibern in objektorientierten Sprachen sehr viel weniger zustandshaft ist. Um dennoch einen Schritt mehr in Richtung „clojuresquem“ Design zu gehen, wurden testweise einige Methoden implementiert, die sich die Methoden zur Manipulation von Collections in Clojure zum Vorbild nehmen. Hier wurden jedoch einige Kritikpunkte gefunden, da sich eine zustandshafte Collection in einer Datenbank grundlegend anders verhält als die Collections in Clojure. Aufgrund dieser Unterschiede ist der Sinn dieser Methoden zweifelhaft, vor allem da diese ein gleiches Verhalten beider Arten von Collections suggerieren. Mit der agilen Herangehensweise bei der Entwicklung von Clarango wurden gute Erfahrungen gemacht. Es konnte schon mit der Entwicklung begonnen werden, während gleichzeitig noch das ArangoDB HTTP-Interface umfassend untersucht wurde und dessen Anforderungen an den Clojure-Treiber noch nicht vollständig klar waren. Dies war einerseits motivierend, da schnell erste Ergebnisse sichtbar waren und nach jedem Entwicklungsschritt eine lauffähige Version von Clarango verfügbar war; andererseits konnten die bisher umgesetzten Features schon evaluiert werden und die daraus gezogenen Schlüsse in das weitere Design einfießen. Was die weitere Entwicklung von Clarango angeht, sind in Zukunft noch einige Verbesserungen denkbar: • Das Error-Handling könnte verbessert werden: Es könnten eigene Java ErrorKlassen entwickelt werden, die bei HTTP-Fehlern (also auch bei Error-Meldungen von ArangoDB) geworfen werden, anstatt hier auf Default-Typen zurückzugreifen. So wäre schon vom Namen der auftretenden Fehler her klar, dass es sich um Fehler handelt, die in Clarango aufgetreten sind. Dies wäre vor allem sinnvoll, wenn 84 Clarango in einer komplexeren Anwendung verwendet wird, die noch andere Libraries verwendet. So kann in der Entwicklung der Anwendung auf einen Blick festgestellt werden, ob der Fehler in einer der Libraries aufgetreten ist, und in welcher, oder ob er vom eigenen Code hervorgerufen wurde. • Andererseits könnte in vielen Fällen die Aussagekraft von Clojure-Fehlermeldungen, die in Clarango auftreten, deutlich verbessert werden, wenn eine Typ-Überprüfung der Eingabeparameter durchgeführt würde. Beim Testen der Clojure API-Methoden passierte es häufiger, dass diese versehentlich mit Parametern eines falschen Typs aufgerufen wurden. Dies war auch häufig der Fall, wenn eigentlich nur die Reihenfolge der Parameter versehentlich vertauscht wurde. Da die Typen hier nicht überprüft werden, werden die Werte einfach an die Funktionen der unteren Level „durchgereicht“ und es tritt ein Fehler bei der ersten Methode auf, die diesen Typ nicht mehr verarbeiten kann. Entsprechend irreführend sind oft die Fehlermeldungen, die in solchen Fällen auftreten. Würden dagegen die Typen überprüft werden, so würden direkt aussagekräftige Fehler an der Quelle des Fehlers auftreten. • Das Testing sollte vervollständigt werden, sodass alle Namespaces von Clarango einen korrespondierenden Test-Namespace erhalten und alle API-Methoden umfassend getestet werden. • Batch-Requests sollten implementiert werden. Dies wurde zwar begonnen, aber nicht fertiggestellt, da hier wie erläutert auf der Ebene der Apache HttpComponents Library gearbeitet werden muss, was die Entwicklungszeit deutlich erhöht. Die Batch-Requests machen aber in der Praxis viel Sinn, besonders als Alternative zu vielen ähnlichen und kurz hintereinander gesendeten DatenbankAnfragen. Die Batch-Funktionalität könnte dann nahtlos in die Clarango API integriert werden, indem man beim Einfügen oder Löschen mehrerer Dokumente statt einem einzelnen Key oder Beispiel-Dokument einen Vektor mit mehreren Dokumenten bzw. mehreren Keys übergibt. So müssten nicht einmal zusätzliche API-Methoden eingeführt werden. • Um den Benutzungskomfort von Clarango zu erhöhen, könnte man es möglich machen, beim Erstellen von Dokumenten und anderen Ressourcen, an den Stellen, die die Namen bzw. Keys von anderen Ressourcen als Parameter erwarten, auch direkt den Rückgabewert der Methode anzugeben, die die entsprechende Ressource erstellt hat. So muss aus dem Rückgabewert letzterer Methoden nicht das :_key Attribut herausgefiltert werden. Wird also statt einem Keyword bzw. einem String eine Map übergeben, sucht Clarango automatisch in dieser Map nach dem :_key Attribut. Dies würde unter anderem die Erstellung von Graphen deutlich 85 vereinfachen. Aus gerade erstellten Knoten müssten nicht mehr die generierten Keys herausgefiltert werden, sondern es könnte gleich der ganze Rückgabewert der graph/create-vertex Methode an die graph/create-edge Methode übergeben werden, um eine Kante zu erstellen106. Diese Option zur Verbesserung des Benutzungskomforts steht jedoch in Konfikt mit der oben erwähnten möglichen Überprüfung der Eingabeparameter der Clarango API-Methoden. Wenn statt Strings auch an jeder Stelle alternativ Maps übergeben werden können, so kann kein Fehler mehr aufgrund eines falschen Typs geworfen werden. • Die Query API von Clarango könnte in einer mehr „clojuresquen“ Variante umgesetzt werden, nach dem Vorbild wie zum Beispiel bei clj-gremlin und bei der Query DSL von Monger. So könnten komplexe Queries auch durch das Ineinanderschachteln von Methodenaufrufen zusammengesetzt werden. • Zusätzlich könnte zur Geschwindigkeitserhöhung von ArangoDBs Unterstützung asynchroner Requests („Fire and Forget“ Strategie 107) Gebrauch gemacht werden. Damit können mehrere Anfragen auf einmal an die Datenbank gesendet werden, ohne dass der Client auf die Antwort des Servers warten muss. Somit wird dieser nicht blockiert, während die Anfrage auf dem Server bearbeitet wird. Stattdessen wird der Request auf dem Datenbank-Server zunächst in einer Queue gespeichert und die Antwort kann dann nach der Bearbeitung durch den Client „abgeholt“ werden. Hier könnte man sich dann auch evtl. Clojures besondere Stärken im Bereich der nebenläufigen Programmierung zunutze machen, die bisher gar nicht für Clarango genutzt wurden. Zu beachten ist jedoch, dass für das Abholen der Antworten ein Polling notwendig ist und außerdem der Server bei jeder gesendeten Anfrage zunächst eine inhaltslose Antwort mit dem HTTP-Statuscode 202 zurücksendet. Die Anzahl der gesendeten HTTP-Requests würde sich somit bei dieser Technik stark erhöhen und somit auch der Gesamt-Overhead des Treibers, der ja ursprünglich möglichst gering gehalten werden sollte. Der Nutzen eines solchen Features müsste somit sorgfältig abgewogen werden. Wie bereits am Anfang dieser Bachelorarbeit erwähnt, arbeiten die Entwickler von ArangoDB kontinuierlich an der Datenbank und ihren Funktionen weiter. Mit der Version 2108 von ArangoDB wird ein weiteres bedeutendes Feature hinzugefügt: das sogenannte Sharding. Mit diesem ist es möglich, eine Collection, sollte sie sehr groß sein, über mehrere Datenbank-Server aufzuteilen. Damit wird in ArangoDB auch erstmals eine horizontale 106Vgl. hierzu auch das Codebeispiel in Neocons aus Abschnitt 4.4.6: „Create Vertices und Edge ...“ 107Vgl. http://www.arangodb.org/manuals/current/HttpJob.html 108https://www.arangodb.org/2014/03/05/arangodb-sharding-release-2-0-0-rc1 86 Skalierung in Dimensionen wie zum Beispiel bei MongoDB möglich. Da ArangoDB auch heute schon über eine Vielzahl an Funktionen verfügt und ein wirkliches Allround-Talent unter den NoSQL Datenbanken darstellt, ist zu erwarten, dass sich die Nutzerzahl vermutlich bald deutlich erhöhen wird. Vielleicht wird gar irgendwann das erklärte Ziel der Entwickler erreicht und ArangoDB wird zum „MySQL in NoSQL“. In diesem Fall würde möglicherweise auch der im Rahmen dieser Bachelorarbeit entwickelte Treiber Clarango zu einer breiteren Nutzung gelangen, da die Zukunft der noch jungen Sprache Clojure ebenfalls vielversprechend aussieht. 87 8. Abbildungsverzeichnis • Abbildung 1: Diagramm eines funktionalen Programms; nach Abbildung 2-2 auf S. 81 in [Emerick2012] – S.11 • Abbildung 2: Clarango Source Dateistruktur; Screenshot aus dem Mac Finder; – S.70 • Abbildung 3: Clarango Architektur und Abhängigkeiten der Namespaces – S.72 • Abbildung 4: Ablaufdiagramm: Aufruf einer Methode aus dem document Namespace (allgemein) – S.73 • Abbildung 5: Ablaufdiagramm: Aufruf einer Methode aus dem document Namespace (speziell) – S.78 Sofern nicht anders angegeben, handelt es sich um selbst erstellte Grafiken. 88 9. Quellenverzeichnis 9.1 Buchquellen [Edlich2011] Stefan Edlich, Achim Friedland, Jens Hampe, Benjamin Brauer, Markus Brückner: NoSQL: Einstieg in die Welt nichtrelationaler Web 2.0 Datenbanken, 2., aktualisierte und erweiterte Aufage, 1. September 2011, Hanser, München [Emerick2012] Chas Emerick, Brian Carper, Christophe Grand: Clojure Programming, erste Aufage, 28. März 2012, O'Reilly Media, Sebastopol [Fielding2000] Roy Thomas Fielding: Architectural Styles and the Design of Network-based Software Architectures, Dissertation, 2000, University of California, Irvine [Redmond2012] Eric Redmond, Jim R. Wilson: Seven Databases in Seven Weeks: A Guide to Modern Databases and the NoSQL Movement, Juni 2012, Pragmatic Programmers, LLC. [Richardson2007] Leonard Richardson, Sam Ruby: Web Services mit REST, erste Aufage, 2007, O'Reilly Verlag, Köln [Sadalage2013] Pramodkumar J. Sadalage, Martin Fowler: NoSQL Distilled: A Brief Guide to the Emerging World of Polyglot Persistence 2013, Addison-Wesley, Upper Saddle River, New Jersey 89 [Tate2011] Bruce A. Tate: Sieben Wochen, sieben Sprachen: Verstehen Sie die modernen Sprachkonzepte, erste Aufage, 2011, O'Reilly Verlag, Köln [Tilkov2009] Stefan Tilkov: REST und HTTP, erste Aufage, 2009, dpunkt.verlag, Heidelberg 9.2 Internetquellen [wwwArangoAPIAQL] ArangoDB: HTTP Interface for AQL Queries http://www.arangodb.org/manuals/current/HttpQuery.html (abgerufen am 21.01.2014) [wwwArangoAPIColl] ArangoDB: HTTP Interface for Collections http://www.arangodb.org/manuals/current/HttpCollection.html (abgerufen am 15.01.2014) [wwwArangoAPIDB] ArangoDB: HTTP Interface for Databases http://www.arangodb.org/manuals/current/HttpDatabase.html (abgerufen am 18.01.2014) [wwwArangoAPIDoc] ArangoDB: HTTP Interface for Documents http://www.arangodb.org/manuals/current/RestDocument.html (abgerufen am 04.01.2014) [wwwArangoAPIEdge] ArangoDB: HTTP Interface for Edges http://www.arangodb.org/manuals/current/RestEdge.html (abgerufen am 14.01.2014) [wwwArangoAPIGraph] ArangoDB: HTTP Interface for Graphs http://www.arangodb.org/manuals/current/HttpGraph.html 90 (abgerufen am 14.01.2014) [wwwArangoAPINaming] Naming Conventions in ArangoDB http://www.arangodb.org/manuals/current/NamingConventions.html (abgerufen am 05.01.2014) [wwwArangoAPITrav] ArangoDB: HTTP Interface for Traversals http://www.arangodb.org/manuals/current/HttpTraversals.html (abgerufen am 16.01.2014) [wwwArangoBlog1] ArangoDB Blog: 7 reasons why ArangoDB is the world‘s best nosql database (or even better than that ;-)) https://www.arangodb.org/2012/03/07/7-reasons-why-avocadodb-is-the-worlds-best-nosqldatabase-or-even-better-than-that (abgerufen am 27.01.2014) [wwwArangoBlog2] ArangoDB Blog: ArangoDB’s design objectives https://www.arangodb.org/2012/03/07/avocadodbs-design-objectives (abgerufen am 27.01.2014) [wwwArangoBlog3] ArangoDB Blog: Infographic – comparing the disk space usage of MongoDB, CouchDB and ArangoDB https://www.arangodb.org/2012/07/11/infographic-comparing-space-usage-mongodb-couchdbarangodb (abgerufen am 28.01.2014) [wwwArangoBlog4] ArangoDB Blog: Benchmarking ArangoDB's networking and HTTP-layer https://www.arangodb.org/2012/07/02/benchmarking-arangodbs-networking-http-layer (abgerufen am 29.01.2014) [wwwArangoBlog5] ArangoDB Blog: Gain factor of 5 using batch requests https://www.arangodb.org/2012/10/04/gain-factor-of-5-using-batch-updates (abgerufen am 29.01.2014) 91 [wwwArangoBlog6] ArangoDB Blog: Bulk inserts in MongoDB, CouchDB, and ArangoDB https://www.arangodb.org/2012/09/04/bulk-inserts-mongodb-couchdb-arangodb (abgerufen am 29.01.2014) [wwwArangoFAQ] ArangoDB FAQ http://www.arangodb.org/faq (abgerufen am 26.01.2014) [wwwArangoFoxx] ArangoDB Foxx http://www.arangodb.org/foxx (abgerufen am 28.01.2014) [wwwArangoFS] First Steps with ArangoDB http://www.arangodb.org/manuals/current/FirstStepsArangoDB.html (abgerufen am 28.01.2014) [wwwArangoManAuth] ArangoDB's User Manual: Authentication and Authorisation https://www.arangodb.org/manuals/current/DbaManualAuthentication.html (abgerufen am 27.01.2014) [wwwArangoManIndex] ArangoDB's User Manual: Handling Indexes https://www.arangodb.org/manuals/current/HandlingIndexes.html (abgerufen am 27.01.2014) [wwwArangoManRep] ArangoDB's User Manual: Replication http://www.arangodb.org/manuals/current/UserManualReplication.html (abgerufen am 28.01.2014) [wwwArangoTalk1] Lucas Dohmen: ArangoDB – a different approach to NoSQL, Vortrag bei der NoSQL matters Conference 2013 in Barcelona, Vortrag: http://www.youtube.com/watch?v=eB-YHgMT2D0, Slides: http://2013.nosql-matters.org/bcn/wp-content/uploads/2013/12/ArangoDB.pdf (abgerufen am 25.01.2014) 92 [wwwArangoTalk2] Martin Schönert: AvocadoDB109 explained, Vortrag bei der NoSQL matters Conference, http://vimeo.com/36411892 (abgerufen am 25.01.2014) [wwwCarmine] Carmine Github Repository und Readme https://github.com/ptaoussanis/carmine (abgerufen am 14.02.2014) [wwwClj-Orient] clj-orient Github Repository und Readme http://www.github.com/eduardoejp/clj-orient (abgerufen am 16.02.2014) [wwwClojure] Clojure.org Hauptseite http://clojure.org (abgerufen am 16.01.2014) [wwwClojureRatio] Clojure.org Rationale http://clojure.org/rationale (abgerufen am 07.01.2014) [wwwClutch] Clutch Github Repository und Readme https://github.com/clojure-clutch/clutch (abgerufen am 16.02.2014) [wwwClutchGroup] Google Groups: Clojure Clutch > Working on a CouchDB type https://groups.google.com/forum/#!topic/clojure-clutch/RSBxbrN6kMw (abgerufen am 17.02.2014) [wwwElasticGlossary] Elasticsearch reference: glossary of terms http://www.elasticsearch.org/guide/en/elasticsearch/reference/current/glossary.html (abgerufen am 18.11.2013 ) 109„AvocadoDB“ war der frühere Name von ArangoDB 93 [wwwElastischDocs1] Elastisch Doc Guides: Mappings and Indexing http://clojureelasticsearch.info/articles/indexing.html (abgerufen am 18.11.2013 ) [wwwElastischDocs2] Elastisch Doc Guides: Getting Started http://clojureelasticsearch.info/articles/getting_started.html (abgerufen am 18.11.2013 ) [wwwHTTP1999] Fielding, et al.: Hypertext Transfer Protocol – HTTP/1.1 (RFC 2616), Juni 1999, http://tools.ietf.org/rfc/rfc2616.txt (abgerufen am 03.12.2013 ) [wwwHTTP2005] M. Nottingham, J. Mogul: HTTP Header Field Registrations (RFC 4229), Dezember 2005, http://tools.ietf.org/rfc/rfc4229.txt (abgerufen am 03.12.2013 ) [wwwHTTP2010] L. Dusseault, Linden Lab, J. Snell: PATCH Method for HTTP (RFC 5789), März 2010, http://tools.ietf.org/html/rfc5789 (abgerufen am 07.01.2014 ) [wwwMongerAPI] Monger API Documentation http://reference.clojuremongodb.info/ (abgerufen am 18.11.2013 ) [wwwMongerDocs1] Monger Doc Guides: Getting Started http://clojuremongodb.info/articles/getting_started.html (abgerufen am 18.11.2013 ) [wwwMongerDocs2] Monger Doc Guides: Querying: finders and query DSL http://clojuremongodb.info/articles/querying.html (abgerufen am 18.11.2013 ) 94 [wwwMongerDocs3] Monger Doc Guides: Indexing and other collection operations http://clojuremongodb.info/articles/collections.html (abgerufen am 18.11.2013 ) [wwwMongerDocs4] Monger Doc Guides: Querying http://clojuremongodb.info/articles/querying.html (abgerufen am 16.02.2013 ) [wwwMongoDBCRUD] MongoDB CRUD Introduction http://docs.mongodb.org/manual/core/crud-introduction/ (abgerufen am 18.11.2013 ) [wwwNeoconsGuide] Neocons Guides: Getting started http://clojureneo4j.info/articles/getting_started.html (abgerufen am 13.03.2014) [wwwOrientDB1] Tutorial: Document and graph model https://github.com/orientechnologies/orientdb/wiki/Tutorial%3A-Document-and-graph-model (abgerufen am 16.02.2014) [wwwTravisGuide] Travis CI Guides: Getting started http://docs.travis-ci.com/user/getting-started/ (abgerufen am 09.03.2014) [wwwVersioning] Semantic Versioning 2.0.0 http://semver.org/ (abgerufen am 06.03.2014) [wwwW3CArchitecture] W3C Recommendation: Architecture of the World Wide Web, Volume One, W3C Technical Architecture Group, 15.12.2004, http://www.w3.org/TR/2004/REC-webarch-20041215 (abgerufen am 07.12.2013) 95 10. Anhang 10.1 Ausgaben des Anwendungsbeispiels aus Kapitel 6 Hier sollen noch einmal zur Verdeutlichung die Ausgaben des Anwendungsbeispiels aus Kapitel 6 gegeben werden. Die Ausgaben entsprechen exakt den Ausgaben der MainMethode, die zu finden ist unter110: https://github.com/edlich/clarango/blob/1196614025ef32be4ace5f2fc91f4cf968f8c825/src /clarango/main.clj Zusätzlich wurde noch die Ausgabe der Ressourcen-URIs im Namespace http-utility aktiviert111. ---- first create a database and a collection and make some document CRUD ---- connect to defaults: localhost and port 8529 create Database 'test-DB' POST connection address: http://localhost:8529/_db/_system/_api/database {"result" true, "error" false, "code" 200} create Collection 'test-collection' in DB 'test-DB' POST connection address: http://localhost:8529/_db/test-DB/_api/collection {"isVolatile" false, "error" false, "name" "test-collection", "code" 200, "waitForSync" false, "status" 3, "isSystem" false, "type" 2, "id" "1108492711"} document CRUD POST connection address: http://localhost:8529/_db/test-DB/_api/document/? collection=test-collection {"error" false, "_id" "test-collection/test-doc", "_rev" "1109344679", 110Zu beachten ist hier, dass bei wiederholten Aktionen wie zum Beispiel dem Erstellen mehrerer Collections oder Dokumente hintereinander teilweise auf Ausgaben verzichtet wurde, um Wiederholungen zu vermeiden. 111Die Ausgabe wird mittels eines Boolean-Werts hier aktiviert: https://github.com/edlich/clarango/blob/master/src/clarango/utilities/http_utility.clj#L22 96 "_key" "test-doc"} PATCH connection address: http://localhost:8529/_db/test-DB/_api/document/test- collection/test-doc {"error" false, "_id" "test-collection/test-doc", "_rev" "1109737895", "_key" "test-doc"} GET connection address: http://localhost:8529/_db/test-DB/_api/document/test- collection/test-doc {"name" "some test document", "additional" "some additional info", "_id" "test-collection/test-doc", "_rev" "1109737895", "_key" "test-doc"} PUT connection address: http://localhost:8529/_db/test-DB/_api/simple/replace-by- example {"replaced" 1, "error" false, "code" 200} ---- now make use of the clojure idiomatic methods available in the namespace collection-ops to add and delete more content in the collection ---- set default DB; this database will be used in the following methods without explicitely having to pass it collection ops : assoc, dissoc, conj POST connection address: http://localhost:8529/_db/test-DB/_api/document/? collection=test-collection {"error" false, "_id" "test-collection/new-document-1", "_rev" "1110589863", "_key" "new-document-1"} POST connection address: http://localhost:8529/_db/test-DB/_api/document/? collection=test-collection {"error" false, "_id" "test-collection/1110786471", "_rev" "1110786471", "_key" "1110786471"} GET connection address: http://localhost:8529/_db/test-DB/_api/document/test- collection/new-document-1 {"key-type" "given key", "description" "some test document to test the clojure idiomatic collection methods", "_id" "test-collection/new-document-1", "_rev" "1110589863", "_key" "new-document-1"} DELETE connection address: http://localhost:8529/_db/test-DB/_api/document/test- collection/new-document-1 97 {"error" false, "_id" "test-collection/new-document-1", "_rev" "1110589863", "_key" "new-document-1"} ---- modify the collection ---- get information about the collection and a list of all documents inside it GET connection address: http://localhost:8529/_db/test-DB/_api/collection/test- collection {"id" "1108492711", "name" "test-collection", "status" 3, "type" 2, "error" false, "code" 200} GET connection address: http://localhost:8529/_db/test-DB/_api/document/? collection=test-collection ["/_api/document/test-collection/test-doc" "/_api/document/test-collection/1110786471"] rename the collection and modify it's properties PUT connection address: http://localhost:8529/_db/test-DB/_api/collection/test- collection/rename {"id" "1108492711", "name" "new-name-test-collection", "status" 3, "type" 2, "error" false, "code" 200} PUT connection address: http://localhost:8529/_db/test-DB/_api/collection/new-name- test-collection/properties {"isVolatile" false, "error" false, "name" "new-name-test-collection", "code" 200, "waitForSync" false, "status" 3, "doCompact" true, "journalSize" 33554432, "isSystem" false, "type" 2, "id" "1108492711", "keyOptions" {"type" "traditional", "allowUserKeys" true}} GET connection address: http://localhost:8529/_db/test-DB/_api/collection/new-name- test-collection/figures 98 {"isVolatile" false, "error" false, "name" "new-name-test-collection", "code" 200, "count" 2, "figures" {"alive" {"count" 2, "size" 302}, "dead" {"count" 3, "size" 451, "deletion" 1}, "datafiles" {"count" 0, "fileSize" 0}, "journals" {"count" 1, "fileSize" 33554432}, "compactors" {"count" 0, "fileSize" 0}, "shapefiles" {"count" 1, "fileSize" 2097152}, "shapes" {"count" 9}, "attributes" {"count" 4}}, "waitForSync" false, "status" 3, "doCompact" true, "journalSize" 33554432, "isSystem" false, "type" 2, "id" "1108492711", "keyOptions" {"type" "traditional", "allowUserKeys" true}} unload and delete collection PUT connection address: http://localhost:8529/_db/test-DB/_api/collection/new-name- test-collection/unload {"id" "1108492711", "name" "new-name-test-collection", "status" 4, "type" 2, "error" false, "code" 200} DELETE connection address: http://localhost:8529/_db/test-DB/_api/collection/new- name-test-collection {"id" "1108492711", "error" false, "code" 200} ---- now create a graph, query it's vertices and perform some graph operations including a traversal ---- first create another Database 'GraphTestDB' POST connection address: http://localhost:8529/_db/_system/_api/database now list all available databases GET connection address: http://localhost:8529/_db/_system/_api/database ["GraphTestDB" "_system" "test-DB"] perform next operations in the context of 'GraphTestDB' 99 create vertex and edge collections 'people' and 'connections' POST connection address: http://localhost:8529/_db/GraphTestDB/_api/collection POST connection address: http://localhost:8529/_db/GraphTestDB/_api/collection now list all available collections, excluding the system collections GET connection address: http://localhost:8529/_db/GraphTestDB/_api/collection [{"id" "1124417959", "name" "connections", "status" 3, "type" 3} {"id" "1123762599", "name" "people", "status" 3, "type" 2}] create graph 'test-graph' POST connection address: http://localhost:8529/_db/GraphTestDB/_api/graph {"_id" "_graphs/test-graph", "_rev" "1125859751", "_key" "test-graph", "vertices" "people", "edges" "connections"} now get all available graphs GET connection address: http://localhost:8529/_db/GraphTestDB/_api/graph [{"_id" "_graphs/test-graph", "_rev" "1125859751", "_key" "test-graph", "vertices" "people", "edges" "connections"}] perform next operations in the context of the graph 'test-graph' create vertices 'Peter', 'Bob', 'Clara', 'Jessica', 'Alice' with :ages POST connection address: http://localhost:8529/_db/GraphTestDB/_api/graph/test- graph/vertex POST connection address: http://localhost:8529/_db/GraphTestDB/_api/graph/test- graph/vertex POST connection address: http://localhost:8529/_db/GraphTestDB/_api/graph/test- graph/vertex POST connection address: http://localhost:8529/_db/GraphTestDB/_api/graph/test- graph/vertex POST connection address: http://localhost:8529/_db/GraphTestDB/_api/graph/test- graph/vertex ---- perform query: find all people who are older than 24 first validate the query, then explain (how the query would be executed on the server), then actually execute it ---POST connection address: http://localhost:8529/_api/query {"bindVars" [], "collections" ["people"], "error" false, "code" 200} POST connection address: http://localhost:8529/_db/GraphTestDB/_api/explain 100 [{"id" 1, "loopLevel" 1, "type" "for", "resultVariable" "p", "expression" {"type" "collection", "value" "people", "extra" {"accessType" "all"}}} {"id" 2, "loopLevel" 1, "type" "filter", "expression" {"type" "expression", "value" "p.age > 24"}} {"id" 3, "loopLevel" 1, "type" "return", "expression" {"type" "reference", "value" "p"}}] POST connection address: http://localhost:8529/_db/GraphTestDB/_api/cursor {"result" [{"_id" "people/peter", "_rev" "1126580647", "_key" "peter", "age" 25, "name" "Peter"} {"_id" "people/bob", "_rev" "1126973863", "_key" "bob", "age" 28, "name" "Bob"} {"_id" "people/clara", "_rev" "1127301543", "_key" "clara", "age" 29, "name" "Clara"}], "hasMore" false, "error" false, "code" 201} create edges with labels 'friend', 'boyfriend', 'girlfriend'; save one key to use this edge later POST connection address: http://localhost:8529/_db/GraphTestDB/_api/graph/test- graph/edge POST connection address: http://localhost:8529/_db/GraphTestDB/_api/graph/test- graph/edge POST connection address: http://localhost:8529/_db/GraphTestDB/_api/graph/test- graph/edge POST connection address: http://localhost:8529/_db/GraphTestDB/_api/graph/test- graph/edge POST connection address: http://localhost:8529/_db/GraphTestDB/_api/graph/test- graph/edge 101 get vertices that have connections going from the vertex 'peter' POST connection address: http://localhost:8529/_db/GraphTestDB/_api/graph/test- graph/vertices/peter {"result" [{"_id" "people/alice", "_rev" "1128022439", "_key" "alice", "age" 20, "name" "Alice"}], "hasMore" false, "count" 1, "error" false, "code" 201} update one edge PATCH connection address: http://localhost:8529/_db/GraphTestDB/_api/graph/test- graph/edge/1128874407 {"_id" "connections/1128874407", "_rev" "1132413351", "_key" "1128874407", "_from" "people/peter", "_to" "people/alice", "$label" "friend", "description" "Peter and Alice have been friends for over 6 years"} get all edges that are outgoing from the vertex 'peter' POST connection address: http://localhost:8529/_db/GraphTestDB/_api/graph/test- graph/edges/peter {"result" [{"_id" "connections/1128874407", "_rev" "1132413351", "_key" "1128874407", "_from" "people/peter", "_to" "people/alice", "$label" "friend", "description" "Peter and Alice have been friends for over 6 years"}], "hasMore" false, "count" 1, "error" false, "code" 201} execute a graph traversal POST connection address: http://localhost:8529/_db/GraphTestDB/_api/traversal {"vertices" [{"_id" "people/peter", "_rev" "1126580647", "_key" "peter", "age" 25, "name" "Peter"}], 102 "paths" [{"edges" [], "vertices" [{"_id" "people/peter", "_rev" "1126580647", "_key" "peter", "age" 25, "name" "Peter"}]}]} delete one edge DELETE connection address: http://localhost:8529/_db/GraphTestDB/_api/graph/test- graph/edge/1128874407 {"deleted" true, "error" false, "code" 202} delete one vertex DELETE connection address: http://localhost:8529/_db/GraphTestDB/_api/graph/test- graph/vertex/peter {"deleted" true, "error" false, "code" 202} delete the graph DELETE connection address: http://localhost:8529/_db/GraphTestDB/_api/graph/test- graph {"deleted" true, "error" false, "code" 200} delete databases DELETE connection address: http://localhost:8529/_db/_system/_api/database/GraphTestDB {"result" true, "error" false, "code" 200} DELETE connection address: http://localhost:8529/_db/_system/_api/database/test-DB 103 10.2 Vollständige Clarango API Dokumentation 10.2.1 Core API connection-set? (connection-set?) Returns true if a connection is set. get-connection (get-connection) Returns the db server connection map to other namespaces. set-connection! (set-connection!) (set-connection! connection-map) Connects permanently to an ArangoDB host by setting the connection map as a global variable. If called without arguments set default connection at localhost:8529 with _system db. set-connection-url! (set-connection-url! connection-url) Sets the server url. set-default-collection! (set-default-collection! collection-name) Sets a default collection. set-default-db! (set-default-db! database-name) Sets a default database. set-default-graph! (set-default-graph! graph-name) Sets a default graph. with-collection (with-collection collection-name & body) Dynamically rebinds the default collection value. Takes a body of code which will be executed in the context of this collection. 104 with-connection (with-connection connection & body) Dynamically rebinds the global connection map. Takes a body of code which will be executed in the context of this connection. with-db (with-db database-name & body) Dynamically rebinds the default database value. Takes a body of code which will be executed in the context of this database. with-graph (with-graph graph-name & body) Dynamically rebinds the default graph value. Takes a body of code which will be executed in the context of this graph. 10.2.2 Document API create (create document & args) Creates a document. First argument: A map that represents the document. If you want to specify a key by yourself, add it as the :_key parameter to the document map. If you would like the key to be created automatically, just leave this parameter out. Takes optional a collection name and a db name as further arguments. If omitted by user, the default db and collection will be used. Also optional as argument is another map containing further options: {'createCollection' true/false, 'waitForSync' true/false} (replace the single quotes with double quotes) - createCollection meaning if the collection should be created if it does not exist yet; - waitForSync meaning if the server response should wait until the document is saved to disk; The option map might be passed in an arbitrary position after the first argument. create-multi (create-multi documents & args) Creates multiple documents at a time. First argument is a vector of documents. Takes optional a collection name and a db name as further arguments. If omitted by user, the default db and collection will be used. 105 delete-by-example (delete-by-example example & args) Deletes a document or a number of documents out of a collection by giving an example to match. Takes the example as a map as first argument. Takes optional a collection name and a db name as further arguments. If omitted by user, the default db and collection will be used. Also optional as argument is another map containing further options: {'waitForSync' true/false, 'limit' limit} (replace the single quotes with double quotes) - waitForSync meaning if the server response should wait until the document is saved to disk - limit meaning the maximum amount of documents that will be deleted The option map might be passed in an arbitrary position after the first two arguments. delete-by-key (delete-by-key & args) Deletes a document by its id. Takes the document key as first argument. Takes optional a collection name and a db name as further arguments. If omitted by user, the default db and collection will be used. Also optional as argument is another map containing further options: {'waitForSync' true/false, 'rev' revision_id, 'policy' 'error/last'} (replace the single quotes with double quotes) - waitForSync meaning if the server response should wait until the document is saved to disk; - rev is the document revision - policy meanins the desired behaviour in case the given revision number does not match the latest document revision -> 'error' meaning that an error is thrown if the given revision_id does not match the revision_id in the document -> 'last' meaning the document is still deleted even if the given revision_id does not match the revision_id in the document The option map might be passed in an arbitrary position after the first argument. get-by-example (get-by-example example & args) Gets a document or a number of documents out of a collection by giving an example to match. Takes the example as a map as first argument. Takes optional a collection name and a db name as further arguments. If omitted by user, the default db and collection will be used. 106 Also optional as argument is another map containing further options: {'skip' skip, 'limit' limit} (replace the single quotes with double quotes) - skip meaning the (number of?) documents to skip in the result - limit meaning the maximum amount of documents to return The option map might be passed in an arbitrary position after the first two arguments. get-by-key (get-by-key & args) Gets a document by its key. Takes the document key as first argument. Takes optional a collection name and a db name as further arguments. If omitted by user, the default db and collection will be used. Also optional as argument is another map containing further options: {'rev' revision_id} (replace the single quotes with double quotes) - rev is the document revision; if the current document revision_id does not match the given one, an error is thrown The option map might be passed in an arbitrary position after the first two arguments. get-first-by-example (get-first-by-example example & args) Gets the first document out of a collection that matches an example. Takes the example as a map as first argument. Takes optional a collection name and a db name as further arguments. If omitted by user, the default db and collection will be used. get-info (get-info & args) Gets information about a document by its key. Takes the document key as first argument. Takes optional a collection name and a db name as further arguments. If omitted by user, the default db and collection will be used. Also optional as argument is another map containing further options: {'rev' revision_id, 'policy' 'error/last'} (replace the single quotes with double quotes) - rev is the document revision - policy meaning the desired behaviour in case the given revision number does not match the latest document revision -> 'error' meaning that an error is thrown if the given revision_id does not match the revision_id in the document -> 'last' meaning the document is still returned even if the given revision_id does not match the revision_id in the document 107 The option map might be passed in an arbitrary position after the first two arguments. replace-by-example (replace-by-example new-document example & args) Replaces a document or a number of documents out of a collection by giving an example to match. First argument: A map representing the new document. Second argument: The example map. Takes optional a collection name and a db name as further arguments. If omitted by user, the default db and collection will be used. Also optional as argument is another map containing further options: {'waitForSync' true/false, 'limit' limit} (replace the single quotes with double quotes) - waitForSync meaning if the server response should wait until the document is saved to disk - limit meaning the maximum amount of documents that will be replaced The option map might be passed in an arbitrary position after the first two arguments. replace-by-key (replace-by-key new-document & args) Replaces a document with a map representing the new document. First argument: A map representing the new document. Second argument: The document key. Takes optional a collection name and a db name as further arguments. If omitted by user, the default db and collection will be used. Also optional as argument is another map containing further options: {'waitForSync' true/false, 'rev' revision_id, 'policy' 'error/last'} (replace the single quotes with double quotes) - waitForSync meaning if the server response should wait until the document is saved to disk - rev is the document revision - policy meanins the desired behaviour in case the given revision number does not match the latest document revision -> 'error' meaning that an error is thrown if the given revision_id does not match the revision_id in the document -> 'last' meaning the document is still replaced even if the given revision_id does not match the revision_id in the document The option map might be passed in an arbitrary position after the first two arguments. update-by-example (update-by-example document-properties example & args) Updates a document or a number of documents out of a collection by giving an example to match. 108 First argument: A map containing the new key/value pairs. Second argument: The example map. Takes optional a collection name and a db name as further arguments. If omitted by user, the default db and collection will be used. Also optional as argument is another map containing further options: {'waitForSync' true/false, 'limit' limit, 'keepNull' true/false} (replace the single quotes with double quotes) - waitForSync meaning if the server response should wait until the document is saved to disk - limit meaning the maximum amount of documents that will be updated - keepNull meaning if the key/value pair should be deleted in the document The option map might be passed in an arbitrary position after the first two arguments. update-by-key (update-by-key document-properties & args) Updates a document with a number of key value pairs. Inserts them into the existing document. First argument: A map containing the new key/value pairs. Second argument: The document key. Takes optional a collection name and a db name as further arguments. If omitted by user, the default db and collection will be used. Also optional as argument is another map containing further options: {'waitForSync' true/false, 'keepNull' true/false, 'rev' revision_id, 'policy' 'error/last'} (replace the single quotes with double quotes) - waitForSync meaning if the server response should wait until the document is saved to disk; - keepNull meaning if the key/value pair should be deleted in the document if the argument map contains it with a null as value; - rev is the document revision - policy meanins the desired behaviour in case the given revision number does not match the latest document revision -> 'error' meaning that an error is thrown if the given revision_id does not match the revision_id in the document -> 'last' meaning the document is still updated even if the given revision_id does not match the revision_id in the document The option map might be passed in an arbitrary position after the first two arguments. 10.2.3 Collection API create (create collection-name & args) Creates a new collection. Takes the name of the new collection as first argument. Takes optionally a database name and a map containing options as further arguments. 109 These arguments may be passed in arbituary order. If the database name is omitted by the user, the default db will be used. Possible options in the options map are: {'waitForSync' true/false, 'doCompact' true/false, 'journalSize' journal_size, 'isSystem' true/false, 'isVolatile' true/false, 'type' 2/3, 'keyOptions' [...see below...]} (replace the single quotes with double quotes) - waitForSync meaning if the server response should wait until the document is saved to disk - doCompact meaning whether of not the collection will be compacted (default is true) - journalSize is the maximum size of a journal or datafile; must at least be 1 MB; this can limit also the maximum size of a single object - isSystem meaning if a system collection should be created (default is false) - isVolatile meaning if the collection should only be kept in-memory and not made persistent --> keeping the collection in-memory only will make it slightly faster, but restarting the server will cause full loss - type is the type of the collection: 2 = document collection (default), 3 = edges collection - keyOptions: a JSON array containing the following options for key generation: - type is the type of the key generator (currently available are 'traditional' and 'autoincrement') - allowUserKeys true/false means if true the user can supply his own keys on creating a document; when set to false only the key generator will be responsible for creating the keys; - increment is the increment value for the autoincrement key generator (optional) - offset is the initial offset value for the autoincrement key generator (optional) delete (delete collection-name & args) Deletes a collection. Takes the name of the collection to be deleted as first argument. Optionally you can pass a database name as second argument. get-all-documents (get-all-documents & args) Returns a list with the URIs of all documents in the collection. Can be called without arguments. In that case the default collection from the default database will be used. Optionally you can pass a collection name as first and a database name as second argument. 110 get-extended-info (get-extended-info & args) Returns extended information about a collection. Forces a load of the collection. Can be called without arguments. In that case the default collection from the default database will be used. Optionally you can pass a collection name as first and a database name as second argument. get-extended-info-count (get-extended-info-count & args) Returns extended information about a collection including the number of documents in the collection. Forces a load of the collection. Can be called without arguments. In that case the default collection from the default database will be used. Optionally you can pass a collection name as first and a database name as second argument. get-extended-info-figures (get-extended-info-figures & args) Returns extended information about a collection including detailed information about the documents in the collection. Forces a load of the collection. Can be called without arguments. In that case the default collection from the default database will be used. Optionally you can pass a collection name as first and a database name as second argument. get-info (get-info & args) Returns information about a collection. Can be called without arguments. In that case the default collection from the default database will be used. Optionally you can pass a collection name as first and a database name as second argument. load (load & args) Loads a collection into the memory. Returns the collection on success. (?) Can be called without arguments. In that case the default collection from the default database will be loaded. Optionally you can pass a collection name, a database name and a map with options as arguments. 111 Possible options in the options map are: {'count' true/false} - count meaning if the return value should contain the number of documents in the collection -> the default is true, but setting it to false may speed up the request The option map might be passed in an arbitrary position between the other arguments. modify-properties (modify-properties properties & args) Modifies the properties of a collection. As first argument expects a map with options. Takes optional a collection name and a db name as further arguments. If omitted by user, the default db and collection will be used. Possible options in the options map are: {'waitForSync' true/false 'journalSize' size} - waitForSync meaning if the server response should wait until the document is saved to disk - journalSize is the size (in bytes) for new journal files that are created for the collection rename (rename new-name collection-name & args) Renames a collection. On success return a map with properties. First argument: The new collection name Second argument: The old collection name Takes optional a db name as further argument. If omitted by user, the default db will be used. rotate (rotate & args) Rotates the journal of a collection. This means the current journal of the collection will be closed and all data made read-only in order to compact it. New documents will be stored in a new journal. Can be called without arguments. In that case the default collection from the default database will be rotated. Optionally you can pass a collection name as first and a database name as second argument. truncate (truncate & args) Removes all documents from a collection, but leaves the indexes intact. 112 Can be called without arguments. In that case the default collection from the default database will be truncated. Optionally you can pass a collection name as first and a database name as second argument. unload (unload & args) Removes a collection from the memory. On success a map containing collection properties is returned. Can be called without arguments. In that case the default collection from the default database will be truncated. Optionally you can pass a collection name as first and a database name as second argument. 10.2.4 Datenbank API create (create database-name users) Creates a new database. First argument: the name of the new database Second argument: a vector specifying users to initially create for the new database; can be empty; in this case a default user 'root' with an empty password will be created; if not empty, it must contain user objects which may contain the following options: - username: the user name as a string - passwd: the user password as a string; if omitted, an empty password will be set - active: boolean flag indicating whether the user accout should be actived or not; default is true; - extra: an optional map of user information that will be saved, but not interpreted by ArangoDB delete (delete database-name) Deletes a database. Expects the database name of the database to be dropped as argument. get-all-graphs (get-all-graphs & args) Gets a list of all existing graphs within the database. Can be called without arguments. In that case the default database will be used. Optionally you can pass a database name as argument. 113 get-collection-info-list (get-collection-info-list & args) Returns information about all collections in a database as a list. Can be called without arguments. In that case the default database will be used. Optionally you can pass a database and a map with options as arguments. Possible options in the options map are: {'excludeSystem' true/false} - excludeSystem meaning whether or not the system collections should be excluded from the result. get-info-current (get-info-current) Returns information about the current database. get-info-list (get-info-list) Returns a list of all existing databases. get-info-user (get-info-user) Returns a list of all databases the current user can access. Note: this might not work under Windows. 10.2.5 Query API delete-cursor (delete-cursor cursor-id & args) This method deletes a cursor on the server. If you don't intend to make further use of a cursor, you should always delete it to free resources on the server. If all available documents of the query were already retrieved by the client, the cursor was already destroyed automatically. Takes as first argument the id of the cursor to be deleted. The id was returned by the execute and the get-more-results method. Optionally you can pass a database name. If omitted, the default db will be used. execute (execute query-string & args) Executes a query. First argument must be the query string to be executed. 114 If the query references any bind variables, you must additionally pass these in a map as the second argument like this: { 'id' 3 } (replace the single quotes with double quotes) If you don't use any variables, you can leave this out. Optionally you can pass a database name as third (or second) argument. If omitted, the default db will be used. The actual result of the query will be contained in the attribute 'result' as a vector. For more options see the method execute-count. execute-count (execute-count query-string batch-size count & args) Executes a query. Takes also the options 'batch-size' and 'count'. First argument must be the query string to be executed. Second argument must be the batch size. This is the amount of documents that will be returned in the first answer of the server. In case there are more documents, in the server answer there will be the attribute 'hasMore' set to true. In this case you can then use the returned cursor 'id' with the method getmore-results to get the remaining results. Third argument is 'count', a boolean flag indicating whether or not the number of documents that were found for the query should be included in the result of the query as 'count' attribute. This is turned off by default because it might have an influence on the performance of the query. If the query references any bind variables, you must additionally pass these in a map as the fourth argument like this: { 'id' 3 } (replace the single quotes with double quotes) If you don't use any variables, you can leave this out. Optionally you can pass a database name as fifth or fourth argument. If omitted, the default db will be used. The actual result of the query will be contained in the attribute 'result' as a vector. explain (explain query-string & args) Explains how a query would be executed on the server. Returns an execution plan for the query. First argument must be the query string to be evaluated. If the query references any bind variables, you must pass these in a map as second argument like this: { 'id' 3 } (replace the single quotes with double quotes) If you don't use any variables, you can leave the second argument out. 115 Optionally you can pass a database name as third or second argument. If omitted, the default db will be used. get-more-results (get-more-results cursor-id & args) This method gets the remaining results of a query. More results to a query are available if the return value of the execute method contained an attribute 'hasMore' set to true. If after the execution of this method there are still more results to the query, the return value of this method will also contain an attribute 'hasMore' that is set to true. Takes as first argument the id of the cursor that was returned by the execute method. Optionally you can pass a database name. If omitted, the default db will be used. validate (validate query-string) Validates a query without executing it. As a return value you get a map containing the names of the collections and the vars used in the query. If the query is not valid also an error will be thrown including an error message with the problem found in the query. Takes as only argument the query string to be evaluated. 10.2.6 Graph API create (create graph-name vertices-collection edges-collection & args) Creates a new graph. First argument: The name of the graph to be created. Second argument: The name of the collection containing the vertices. Third argument: The name of the collection containing the edges. The ladder two collections must already exist. Optionally you can pass a database name as fourth argument. If omitted, the default db will be used. Also optional as argument is another map containing further options: {'waitForSync' true/false} (replace the single quotes with double quotes) - waitForSync meaning if the server response should wait until the graph has been to disk; 116 create-edge (create-edge edge edge-name vertex-from-name vertex-to-name & args) Creates a new edge. First argument: A map that represents the edge. If you optionally want to specify a label for the edge, you can add it as the :$label parameter to the edge map. Second argument: The name of the edge to be created. Third argument: The name of the from vertex. Fourth argument: The name of the to vertex. Takes optional a graph name and a db name as further arguments. If omitted by user, the default graph and collection will be used. Also optional as argument is another map containing further options: {'waitForSync' true/false} (replace the single quotes with double quotes) - waitForSync meaning if the server response should wait until the edge is saved to disk; The option map might be passed in an arbitrary position after the first four arguments. create-vertex (create-vertex vertex & args) Creates a vertex. First argument: A map that represents the vertex. If you want to specify a key by yourself, add it as the :_key parameter to the vertex map. If you would like the key to be created automatically, just leave this parameter out. Takes optional a graph name and a db name as further arguments. If omitted by user, the default graph and collection will be used. Also optional as argument is another map containing further options: {'waitForSync' true/false} (replace the single quotes with double quotes) - waitForSync meaning if the server response should wait until the vertex is saved to disk; The option map might be passed in an arbitrary position after the first argument. delete (delete graph-name & args) Deletes a graph. Also deletes it's vertex and the edges collection. Takes the name of the graph as first argument. Optionally you can pass a database name as second argument. If omitted, the default db will be used. 117 delete-edge (delete-edge key & args) Deletes an edge. Takes the edge key as first argument. Takes optional a graph name and a db name as further arguments. If omitted by user, the default graph and collection will be used. Also optional as argument is another map containing further options: {'rev' revision_id, 'waitForSync' true/false} (replace the single quotes with double quotes) - rev is the document revision; if the current document revision_id does not match the given one, an error is thrown; - waitForSync meaning if the server response should wait until the action was saved to disk; The option map might be passed in an arbitrary position after the first argument. delete-vertex (delete-vertex key & args) Deletes a vertex. Takes the vertex key as first argument. Takes optional a graph name and a db name as further arguments. If omitted by user, the default graph and collection will be used. Also optional as argument is another map containing further options: {'rev' revision_id, 'waitForSync' true/false} (replace the single quotes with double quotes) - rev is the document revision; if the current document revision_id does not match the given one, an error is thrown; - waitForSync meaning if the server response should wait until the action was saved to disk; The option map might be passed in an arbitrary position after the first argument. execute-traversal (execute-traversal start-vertex vertex-collection edges-collection direction & args) Sends a traversal to the server to execute it. First argument: The key of the start vertex. Second argument: The name of the collection that contains the vertices. Third argument: The name of the collection that contains the edges. Fourth argument: The direction of the traversal. Must be either 'outbound', 'inbound' or 'any'. Can be nil if the 'expander' attribute is set in the additional options. Takes optionally a database name as further argument. If omitted by user, the default database will be used. 118 Also optional as argument is another map containing further options for the traversal: {'filter' {...}, 'expander' code} - see http://www.arangodb.org/manuals/current/HttpTraversals.html#HttpTraversalsPost The option map might be passed in an arbitrary position after the first four arguments. get-edge (get-edge key & args) Gets an edge. Takes the edge key as first argument. Takes optional a graph name and a db name as further arguments. If omitted by user, the default graph and collection will be used. Also optional as argument is another map containing further options: {'rev' revision_id} (replace the single quotes with double quotes) - rev is the document revision; if the current document revision_id does not match the given one, an error is thrown; The option map might be passed in an arbitrary position after the first argument. get-edges (get-edges key batch-size limit count filter & args) Gets several edges. Depending on batch size returns a cursor. First argument: The key of the start edge. Second argument: The batch size of the returned cursor. Third argument: The result size. Fourth argument: An optional filter for the results. If you don't want to use it, just pass nil here. For details on the filter see http://www.arangodb.org/manuals/current/HttpGraph.html#A_JSF_POST_graph_edges Takes optional a graph name and a db name as further arguments. If omitted by user, the default graph and collection will be used. get-info (get-info graph-name & args) Gets info about a graph. Returns a map containing information about the graph. Takes the name of the graph as first argument. Optionally you can pass a database name as second argument. If omitted, the default db will be used. 119 get-vertex (get-vertex key & args) Gets a vertex. Takes the vertex key as first argument. Takes optional a graph name and a db name as further arguments. If omitted by user, the default graph and collection will be used. Also optional as argument is another map containing further options: {'rev' revision_id} (replace the single quotes with double quotes) - rev is the document revision; if the current document revision_id does not match the given one, an error is thrown; The option map might be passed in an arbitrary position after the first argument. get-vertices (get-vertices key batch-size limit count filter & args) Gets several vertices. Depending on batch size returns a cursor. First argument: The key of the start vertex. Second argument: The batch size of the returned cursor. Third argument: The result size. Fourth argument: An optional filter for the results. If you don't want to use it, just pass nil here. For details on the filter see http://www.arangodb.org/manuals/current/HttpGraph.html#A_JSF_POST_graph_vertic es Takes optional a graph name and a db name as further arguments. If omitted by user, the default graph and collection will be used. replace-edge (replace-edge edge-properties key & args) Replaces an edge. First argument: A map containing the new edge. Second argument: The edge key. Takes optional a graph name and a db name as further arguments. If omitted by user, the default graph and collection will be used. Also optional as argument is another map containing further options: {'rev' revision_id, 'waitForSync' true/false} (replace the single quotes with double quotes) - rev is the document revision; if the current document revision_id does not match the given one, an error is thrown; - waitForSync meaning if the server response should wait until the action was saved to disk; The option map might be passed in an arbitrary position after the first argument. 120 replace-vertex (replace-vertex vertex-properties key & args) Replaces a vertex. First argument: A map containing the new vertex. Second argument: The vertex key. Takes optional a graph name and a db name as further arguments. If omitted by user, the default graph and collection will be used. Also optional as argument is another map containing further options: {'rev' revision_id, 'waitForSync' true/false} (replace the single quotes with double quotes) - rev is the document revision; if the current document revision_id does not match the given one, an error is thrown; - waitForSync meaning if the server response should wait until the action was saved to disk; The option map might be passed in an arbitrary position after the first argument. update-edge (update-edge edge-properties key & args) Updates an edge. First argument: A map containing the new edge properties. Second argument: The edge key. Takes optional a graph name and a db name as further arguments. If omitted by user, the default graph and collection will be used. Also optional as argument is another map containing further options: {'rev' revision_id, 'waitForSync' true/false, 'keepNull' true/false} (replace the single quotes with double quotes) - rev is the document revision; if the current document revision_id does not match the given one, an error is thrown; - waitForSync meaning if the server response should wait until the action was saved to disk; - keepNull meaning if the key/value pair should be deleted in the edge if the argument map contains it with a null (nil) as value; The option map might be passed in an arbitrary position after the first argument. update-vertex (update-vertex vertex-properties key & args) Updates a vertex. First argument: A map containing the new vertex properties. Second argument: The vertex key. Takes optional a graph name and a db name as further arguments. If omitted by user, the default graph and collection will be used. 121 Also optional as argument is another map containing further options: {'rev' revision_id, 'waitForSync' true/false, 'keepNull' true/false} (replace the single quotes with double quotes) - rev is the document revision; if the current document revision_id does not match the given one, an error is thrown; - waitForSync meaning if the server response should wait until the action was saved to disk; - keepNull meaning if the key/value pair should be deleted in the vertex if the argument map contains it with a null (nil) as value; The option map might be passed in an arbitrary position after the first argument. 10.2.7 collection-ops API cla-assoc! (cla-assoc! collection-name key val) Adds one document (val) to a collection (specified by collection-name) with a given key. Always uses the default database set in clarango.core. Modeled on core/assoc (http://clojuredocs.org/clojure_core/clojure.core/assoc) Does the same, just on an ArangoDB collection. The difference is that you can currently only pass one key and one document to add to the collection, not several like in clojure.core/dissoc cla-conj! (cla-conj! collection-name x) Adds one document (x) to a collection (specified by collection-name). The key for the document is generated by ArangoDB. Always uses the default database set in clarango.core. Modeled on core/conj (http://clojuredocs.org/clojure_core/clojure.core/conj) Does the same, just on an ArangoDB collection. The difference is that you can currently only pass one element to add to the collection, not several like in clojure.core/conj cla-dissoc! (cla-dissoc! collection-name key) Removes a document thats identified by the key parameter from a collection. Modeled on core/dissoc (http://clojuredocs.org/clojure_core/clojure.core/dissoc) Does the same, just on an ArangoDB collection. The difference is that you can currently only pass one key to remove from the collection, not several like in clojure.core/dissoc cla-get! (cla-get! collection-name key) Gets a document out of a collection by key. 122 Modeled on core/get (http://clojuredocs.org/clojure_core/clojure.core/get) Does the same, just on an ArangoDB collection. Currently this method throws an error when used with a key that does not exist. This should be changed in the future, also it should be possible to give a value that is returned by the function, in case the key does not exist. 123 10.3 ArangoDB API Checkliste Hier folgt eine Aufistung aller Interfaces der ArangoDB HTTP-API112 und welche davon in Clarango umgesetzt wurden; zusammen mit der Angabe, in welchem Namespace der Clarango Implementierung sich die Funktionen jeweils befinden. • HTTP Interface for Databases → umgesetzt in database Namespace • HTTP Interface for Documents → umgesetzt im document Namespace • HTTP Interface for Edges → nicht umgesetzt; es befinden sich jedoch nahezu identische Funktionen im Interface for Graphs, siehe unten • HTTP Interface for AQL Query Cursors → umgesetzt im query Namespace • HTTP Interface for AQL Queries → umgesetzt im query Namespace • HTTP Interface for AQL User Functions Management → nicht umgesetzt • HTTP Interface for Simple Queries → teilweise umgesetzt (-by-example Methoden) im document Namespace • HTTP Interface for Collections → umgesetzt im collection Namespace • HTTP Interface for Indexes → nicht umgesetzt • HTTP Interface for Transactions → nicht umgesetzt • HTTP Interface for Graphs → umgesetzt im graph Namespace • HTTP Interface for Traversals → umgesetzt im graph Namespace 112 Entnommen aus dem „Implementor Manual“: http://www.arangodb.org/manuals/current/ImplementorManual.html 124 • HTTP Interface for Replication → nicht umgesetzt • HTTP Interface for Bulk Imports → nicht umgesetzt • HTTP Interface for Batch Requests → nach einigen Versuchen nicht umgesetzt, da unverhältnismäßig aufwendig • HTTP Interface for Administration and Monitoring → nicht umgesetzt • HTTP Interface for User Management → nicht umgesetzt • HTTP Interface for Async Results Management → nicht umgesetzt • HTTP Interface for Endpoints → nicht umgesetzt • HTTP Interface for Miscellaneous functions → nicht umgesetzt 125