WP Scala WS2010/11: Scala und NoSQL Tom Klonikowski, HAW Hamburg Department Informatik 17.12.2010 Zusammenfassung Neben den traditionellen relationalen Datenbanksystemen haben sich neue Arten von Datenspeichern entwickelt. Vor die Erfordernisse des Web 2.0 führten oenbar zu einem Innovationsschub in diesem Bereich. Dieser Artikel gibt einen Überblick über die alternativen Datenbanktechnologien, die mittlerweile gemeinhin unter dem Begri NoSQL - Not only SQL zusammengefasst werden, und gibt eine Einführung in den Einsatz mit Scala. 1 Motivation Relationale Datenbanken speichern Daten in Datensätzen (n-Tupel), die in Tabellen (zweidimensional) abgelegt werden. In der objektorientierten Programmierung werden jedoch häug sehr komplexe Objekte verwaltet. Sollen diese in einem RDBMS gespeichert werden führt dies zwangsläug zu einer Segmentierung über mehrere verknüpfte Tabellen. Damit einher geht die Notwendigkeit, ein festgelegtes Schema zu erstellen und einzuhalten. Bibliotheken für objekt-relationales Mapping sollen diese Konversion transparent machen, bringen dadurch aber eigene Komplexität in Softwareprojekte. Zudem entwickelten sich durch die Anwendungen des Web 2.0 (soziale Netzwerke, (Micro-)Blogging) besondere Anforderungen an die Datenspeicher. Riesige Datenmengen mit weltweiter Zugrismöglichkeit erfordern ein hohes Maÿ an Skalierbarkeit und Redundanz. Andererseits werden geringere Garantien hinsichtlich der Konsistenz und Transaktionen gefordert. 2 Varianten alternativer Datenbankkonzepte 1 Die neuen Datenbankkonzepte lassen sich wie folgt unterteilen : Dokumentenorientierte Datenbanken : Lotus Notes, CouchDB, MongoDB, Tamino Graphendatenbanken 1 Auszug : AllegroGraph, Core Data, Neo4j, GraphDB aus http://de.wikipedia.org/wiki/NoSQL 1 (Sortierte) Key-Value-Speicher : SimpleDB, BigTable, memcached, Mem- cachedb oder Berkeley DB Multivalue-Datenbanken Objektdatenbanken : OpenQM oder Rocket U2 : Db4o oder ZODB Diese Konzepte bieten jeweils unterschiedliche Vorteile. Während dokumentenorientierte Datenbanken die Möglichkeit bieten, beliebige Dokumente zu speichern, sind es bei Key-Value-Speichern nur Schlüssel-Wert-Paare, die jedoch hochefzient und redundant abgelegt werden können. Objektdatenbanken speichern Objekte direkt und sind daher in der Regel an eine bestimmte Programmiersprache gebunden (Db4o 3 Java, ZODB Python). Dokumentenorientierte Datenbanken Dokumentenorientierte Dankenbanken speichern Dokumente. Ein Dokument kann aus einer Reihe von Key-Value-Paaren bestehen, die geschachtelt werden können, z.B. deniert durch XML oder JSON. Dateien, strukturiert durch ein bestimmtes Dateiformat (z.B. ODF) oder unstrukturiert im Sinn eines Datenzugris (z.B. MP4), können ebenfalls als Dokumente angesehen werden. Die Idee ist, Abbildungen der Entitäten der realen Welt möglichst 1-zu1 in ein Dokument zu überführen und zu speichern. Die Daten einer Entität Rechnung sollen also nicht in Empfänger, Adresse, eine Liste von Rechnungsposten usw. zerlegt und in verschiedenen Tabellen gespeichert werden, sondern in einem Dokument - siehe Beispiel in Abbildung 1 auf der nächsten Seite. Hier ist sicher der Einwand naheliegend, dass der Empfänger bereits als Kunde gespeichert sein wird und es deswegen inezient erscheint, ihn in jeder Rechnung abzulegen. Auch bei dokumentenorientierten Datenbanken ist also eine Verknüpfung von Dokumenten notwendig. Dies ist jedoch ausserhalb des Fokus dieser Einführung. Semistrukturierte Daten lassen sich jedoch schwer für eine eziente Suche oder Auswertungen indexieren. Dazu sind Funktionen notwendig, die die unstrukturierten Daten strukturieren, auch Mapping genannt. 4 Beispiel: CouchDB2 Eine der neuesten dokumentenorientierten Datenbanken ist CouchDB, ein Projekt der Apache Software Foundation, welches mittlerweile in Version 1.0.1 vorliegt. 2 http://couchdb.apache.org 2 <?xml version="1.0" encoding="UTF-8"?> <rechnung nr="12300034"> <datum>20.01.2010</datum> <empfaenger> <name>VEB Massenhuehnereierei Schlottbek</name> <adresse> <strasse>Dioxinstr. 2</strasse> <ort plz="20099">Hamburg</ort> </adresse> </empfaenger> <posten anzahl="12" einheit="t"> <artikel> <bezeichnung>Futterdiesel</bezeichnung> <preis waehrung="EUR" bezugseinheit="t">199</preis> </artikel> </posten> </rechnung> Abbildung 1: Rechnung als XML-Dokument CouchDB ist ein verteilter Datenbankserver, der Dokumente schemafrei speichert und eine inkrementelle Replikation bietet. Die Dokumente sind indexierbar und durchsuchbar. Dazu bietet CouchDB die Möglichkeit, Mapping-Funktionen in JavaScript als sogenannte views zu denieren. Der Zugri auf die Datenbank erfolgt über eine REST-JSON-API. Somit werden Dokumente durch POST- oder PUT-Requests geschrieben und durch GET-Requests gelesen. Jedes Dokument aus hat der dabei Adresse eine des eindeutige URL, Datenbankservers, die einem Kollektionsnamen und der Dokumentenid besteht: http://localhost:5984/example/1252231234. So erzeugt ein POST-Request an die Adresse Abbildung 2: CouchDB Komponenten der Kollektion das gesendete Dokument in der Datenbank - siehe Abbildung 3 auf der nächsten Seite. Um das so gespeicherte Dokument zu laden, ist ein GET-Request an die Adresse der Kollektion, erweitert um die zurückgelieferte id des Dokumentes, zu senden (Abbildung 4). Komplexere Abfragen der Datenbank basieren auf dem MapReduce-Prinzip. Dabei wird zunächst eine Mapping-Funktion auf die Daten angewendet, welche die Dokumente strukturiert. Das Ergebnis daraus wird danach durch die ReduceFunktion geltert. Diese Funktionen werden als Query an den Server übergeben. 3 POST http://127.0.0.1:5985/posts/ HTTP/1.0 Content-Length: 245 Content-Type: application/json { } "Subject": "I like Plankton", "Author": "Rusty", "PostedDate": "2006-08-15T17:30:12-04:00", "Tags": [ "plankton", "baseball", "decisions" ], "Body": "I decided today that I don't like baseball. I like plankton." HTTP/1.0 201 Created Server: CouchDB/1.0.1 (Erlang OTP/R13B) Location: http://127.0.0.1:5984/posts/123ad4567 Date: Thu, 16 Dec 2010 20:02:42 GMT Content-Type: text/plain;charset=utf-8 Content-Length: 95 Cache-Control: must-revalidate { } "ok": true, "id": "123ad4567", "rev": "1-1b33f6c3" Abbildung 3: POST-Request zum Anlegen eines neuen Dokumentes und ServerResponse 4 GET http://127.0.0.1:5985/posts/123ad4567 HTTP/1.0 HTTP/1.0 201 Created Server: CouchDB/1.0.1 (Erlang OTP/R13B) Location: http://127.0.0.1:5984/posts/123ad4567 Date: Thu, 16 Dec 2010 20:03:42 GMT Content-Type: text/plain;charset=utf-8 Content-Length: 275 Cache-Control: must-revalidate { } "id": "123ad4567", "Subject": "I like Plankton", "Author": "Rusty", "PostedDate": "2006-08-15T17:30:12-04:00", "Tags": [ "plankton", "baseball", "decisions" ], "Body": "I decided today that I don't like baseball. I like plankton." Abbildung 4: GET-Request zum Laden des Dokumentes und Server-Response 5 CouchDB und Scala Das Erzeugen von HTTP-Requests und das Auslesen der Responses lässt sich unter Scala natürlich mit der Java-Klasse java.net.HttpURLConnection bewerkstelligen. Allerdings lieferte das das Dokument nur als InputStream, der dann selbst geparst werden müsste. Also muss auch hier ein Mapping stattnden: aus dem Dokument im JSON-Format sollen möglichst Instanzen von Klassen werden. Dieses komplexe Problem sollte bedacht werden, bevor die Wahl auf Scala fällt, wenn es um die Verwaltung unstrukturierter Dokumente geht. Hier bieten dynamisch typisierte Sprachen einen ungleich leichteren Zugang. Glücklicherweise gibts es bereits wenigstens eine benutzbare Bibliothek, die 3 dieses Scala-JSON-Databinding übernimmt: scouchdb . Diese verwendet Reection und Annotationen, um JSON in Scala-Objekte zu überführen (Beispiel siehe Abbildung 5 auf Seite 7). 6 Fazit NoSQL-Datenbanken im Allgemeinen und CouchDB im Besonderen sind kein Ersatz für relationale Datenbanken. Sie können aber je nach Aufgabenstellung die bessere Alternative sein. Die Bibliotheken für das notwendige JSON-ScalaDatabinding scheinen noch in den Kinderschuhen zu stecken. Entwicklen sich in diesem Bereich ausgereifte Lösungen, so verbänden sich die Vorteile der statisch typisierten Sprache Scala mit denen der dokumentenorientierten Datenbank 3 https://github.com/debasishg/scouchdb 5 CouchDB. Beim Autor bleibt aber der Eindruck, das die unstrukturierten Dokumente auf der einen und die streng typisierten Objekte auf der anderen Seite ebenfalls ein Impedance Mismatch darstellen. 6 import import import import scouch.db._ scala.reflect._ scala.annotation.target._ dispatch._ @BeanInfo case class Name(first:String, last:String) { private def this() = this(null, null) } @BeanInfo case class Student(matrNr:String, name:Name, semester:Number) { private def this() = this(null, null, null) } def main(args: Array[String]) { val http = new Http // erzeuge Verbindung zur CouchDB (Standard: localhost:5984) val couch = Couch() // Datenbank "example"" val db = Db(couch, "example") // ein neuer Student val s1 = Student("1234567", Name("Max", "Mustermann"), 17) // erzeuge CREATE Request val create = db.doc(s1) // fuehre Request aus // cresponse: Tuple2 (doc_id, rev_id) val cresponse = http(create) val doc_id = cresponse._1 val rev_id = cresponse._2 // erzeuge GET Request fuer eben angelegtes Dokument val get = db.get[Student](doc_id) } // lresponse: Tuple3 (doc_id, rev_id, object) val lresponse = http(get) val s2 = lresponse._3 println(s2) println(s1 == s2) Abbildung 5: Einfaches Beispiel für das Speichern und Laden von Instanzen einer nicht trivialen Case-Klasse 7