NoSQL Datenbanken im Allgemeinen und MongoDB im Speziellen

Veröffentlicht in: Java, NoSQL | 0

Waren Relationale-Datenbanken lange Zeit das Speichermedium der Wahl, so sind in den letzten Jahren NoSQL-Datenbanken (Not only SQL) populär geworden. Diese können und sollen die bestehenden Relationalen-Datenbanken nicht ersetzten, da sie ganz andere Ansätze der Datenspeicherung verfolgen. Es kann aber durchaus Sinn machen, Relationale- und NoSQL-Datenbanken zu kombinieren, um das jeweilige Verhalten optimal zu nutzen. Nicht nur zwischen Relationalen- und NoSQL-Datenbanken gibt es Unterschiede, auch die Ansätze der NoSQL-Datenbanken weichen erheblich voneinander ab. Je nach Einsatzbereich, sind sie auf ihre Aufgabe hin optimiert. Grob können NoSQL-Datenbanken in die folgenden vier Kategorien aufgeteilt werden:

  • Key/Value basierte Datenbanken: Jedem Wert wird ein eindeutiger Schlüssel zugewiesen, der Wert selber wird von der Datenbank nicht interpretiert.
  • Wide Column Store: Hier sind die Spalten einer Tabelle nicht fest definiert. Muss für einen Datensatz eine zusätzliche Information gespeichert werden, kann diese hinzugefügt werden, ohne dass alle bestehenden Datensätze um einen NULL-Wert ergänzt werden. Letzteres wäre beim relationalen Datenmodell der Fall gewesen.
  • Dokumentenorientierte Datenbanken: Es werden ganze Dokumente gespeichert, meistens handelt es sich um XML- oder JSON-Dokumente.
  • Graphen Datenbanken: Diese Datenbanken enthalten Knoten und deren Beziehungen zueinander. Sie sind auf das schnelle Durchlaufen von Graphen ausgelegt.

Einige der NoSQL-Datenbanken sind auf große Mengen an Daten ausgerichtet, daher spielt hier die horizontale Skalierbarkeit eine wichtige Rolle. Bei verteilten Datenbanksystemen kommt nun das CAP-Theorem zum Einsatz, welches besagt, dass bei verteilten Computer Systemen maximal zwei der folgenden drei Eigenschaften erfüllt werden können:

  • Konsistenz (Consistency): Alle Clients sehen zum gleichen Zeitpunkt die gleichen Daten.
  • Verfügbarkeit (Availability): Antwortzeit in der ein Request beantwortet wird.
  • Partitionstoleranz (Partition tolerance): Das System arbeitet weiter, auch wenn einzelne Nachrichten verloren gehen oder einzelne System-Komponenten fehlerhaft arbeiten.

Betrachten wir nun die MongoDB, eine der vielen NoSQL-Datenbanken, genauer. Der Name steht für humongous database – gigantische Datenbank – ist dokumentenorientiert und arbeitet mit JSON ähnlichen Dokumenten. An Anlehnung an JSON heißt das Format BSON – Binary JSON – enthält aber zusätzliche Datentypen wie Date und BinData. Will man die MongoDB nach dem CAP-Theorem einsortieren, so handelt es sich um ein CP-System. Sie hat also Probleme mit Hochverfügbarkeit, während die Daten über verschiedene Knoten Konsistent gehalten werden können.

MongoDB ist aktuell in der Version 2.0.4 verfügbar und kann unter
http://www.mongodb.org/ bezogen werden. Um die MongoDB anschließend unter Windows als Dienst zu registrieren, muss mongod.exe mit folgenden Parametern ausgeführt werden:

mongod.exe –logpath C:mongologslogfilename.log –logappend –dbpath C:mongodata –install

Auch wenn es erstaunen hervorruft, aber unter Windows ist die Angabe der Log-Datei tatsächlich zwingend notwendig.

Unter Linux (Ubuntu) ist es einfacher, hier kann die MongoDB direkt aus der Paketverwaltung installiert werden. MongoDB wird direkt als Service gestartet, die Konfigurations-Datei enthält unter anderem folgende Einträge:

dbpath=/var/lib/mongodb
logpath=/var/log/mongodb/mongodb.log

Die Datei selber ist unter /etc/mongodb.conf zu finden.

Die MongoDB bietet support für diverse Programmiersprachen, u.a für

  • Java
  • C
  • C++
  • .NET
  • PHP

Der aktuelle MongoDB Java Treiber ist in Version 2.7.3 verfügbar und kann bequem als Maven Dependency geladen werden:

<dependency>
<groupId>org.mongodb</groupId>
<artifactId>mongo-java-driver</artifactId>
<version>2.7.3</version>
</dependency>

Um eine Verbindung zur MongoDB aufzubauen, sind lediglich folgende zwei Zeilen nötig:

Mongo mongo = new Mongo();
DB db = mongo.getDB(„myDB“);

Sofern die Datenbank myDB in der MongoDB noch nicht existiert, wird diese automatisch erstellt. Da der Java MongoDB Treiber thread-safe ist, kann beispielsweise in einer Web Anwendung eine einzige Mongo Instanz für die Abarbeitung alle Requests betrieben werden. Sofern nicht anders konfiguriert beinhaltet das Mongo Objekt einen internen Pool von 10 Datenbank-Connections. Bei jeder Anfrage wird eine Connection über den Pool aufgebaut, die Abfrage gesendet und anschließend die Connection wieder abgebaut.

Will man nun seinen ersten Eintrag in der MongoDB speichern, wird ein BasicDBObject erstellt, welchem die Attribute als Key/Value Paare übergeben werden.

BasicDBObject person = new BasicDBObject();
person.put(„forename“, „Max“);
person.put(„lastname“, „Mustermann“);

DBCollection coll = db.getCollection(„persons“);
coll.insert(person);

Anschließend holt man sich über die Datenbank die Collection mit dem Namen persons. In einem Relationalen-Datenbankmodell würde man nicht von Collections, sondern von Datenbank-Tabellen reden. Hat man nun die Collection, kann das bereits generierte BasicDBObject gespeichert werden. Sollte noch keine Collection mit dem Namen persons existieren, wird auch diese automatisch erzeugt.

Will man die Einträge einer Collection erfragen, stehen verschiedene find-Methoden zur Verfügung. Um alle Einträge zu erhalten, wird die find-Methode ohne weitere Parameter – und damit ohne Einschränkungen – aufgerufen:

DBCollection collection = db.getCollection(„persons“);
DBCursor cursor = collection.find();
while (cursor.hasNext()) {
DBObject dbObject = cursor.next();
String forename = (String) dbObject.get(„forename“);
String lastname = (String) dbObject.get(„lastname“);
Person person = new Person(forename, lastname);
}

Die find-Methoden liefern einen DBCursor, mit dem über die Elemente der Collection persons iteriert werden kann. Sind die Parameter des DBObjects bekannt, können diese  – forename und lastname – direkt erfragt werden.

Will man nun die Suche einschränken, wird das gleiche BasicDBObject verwendet, welches schon beim Erzeugen des Eintrags zum Zuge kam. Gewünschte Einschränkungen werden auch hier als Key/Value Paare definiert:

BasicDBObject query = new BasicDBObject();
query.put(„forename“, „Max“);
DBCursor cursor = collection.find(query);

In obigem Beispiel waren die Eigenschaften der Collection persons bekannt, so dass diese direkt über ihren Schlüssel erfragt werden konnten. Da die Eigenschaften einer Collection nicht fest definiert sind und im Laufe der Zeit zusätzliche Key/Value Paare hinzugefügt werden können, müssen auch alle Schlüssel eines DBObjects erfragt werden können:

Set<String> keys = dbObject.keySet();
for (String key : keys) {
Object property = dbObject.get(key);
}

Auch über alle Collections einer Datenbank kann bequem per Java Mongo Treiber API iteriert werden:

Set<String> collectionNames = db.getCollectionNames();
for (String name : collectionNames) {
DBCollection collection = db.getCollection(name);
}

Die Größe von BSON Objekten, die in der MongoDB gespeichert werden können, sind beschränkt. In älteren MongoDB Version liegt die Begrenzung bei 4 MB, ab Versionen 1.7 bei 16 MB. Um dennoch größere Dokumente speichern zu können, wurde die GridFS-Spezifikation zum Speichern von großen Objekten eingeführt. Diese werden über zwei Collections realisiert. In der Collection files sind die Meta-Informationen zu den Daten enthalten, wie Dateiname und Content-Type. Die Objekt-Daten werden zu chunks von ca. 256 k Größe gesplittet und in der gleichnamigen Collection chunks abgelegt. D.h. jede Datei hat einen Eintrag in files und mindestens einen Eintrag in chunks.

Die Speicherung von Dateien über GridFS ist in Java schnell umgesetzt:

InputStream inputStream = new FileInputStream(new File(„somepic.jpg“));
GridFS storeGridFS = new GridFS(db);
GridFSInputFile gridFSInputFile = storeGridFS.createFile(inputStream);
gridFSInputFile.setFilename(„somepic.jpg“);
gridFSInputFile.setContentType(„image/jpeg“);
gridFSInputFile.save();

Über die erzeugte GridFS-Instanz erhält man Zugriff auf das Standard-GridFS der Datenbank. Alternativ kann zusätzlich ein Name angegeben werden, um so ein neues GridFS zu erzeugen. Nun wird der Inhalt der Datei per InputStream der createFile-Methode übergeben. Hier erhält man einen GridFSInputFile, dem Meta-Informationen wie Dateiname und Content-Type übergeben werden können. Diese Informationen werden nach dem Aufruf der save-Methode in der oben erwähnten Collection files persistiert. Die Datei selber wird in chunks aufgeteilt und in eben dieser Collection gespeichert.

Auch das Auslesen der Datei ist schnell implementiert:

GridFS loadedGridFS = new GridFS(db);
List<GridFSDBFile> gridFSDBFiles = loadedGridFS.find(„somepic.jpg“);
GridFSDBFile gridFSDBFile = gridFSDBFiles.get(0);
InputStream in = gridFSDBFile.getInputStream();

Sind Meta-Informationen über die Datei bekannt, wie hier der Dateiname, so kann der GridFSDBFile über das Standard-GridFS geladen werden. Nun stehen die Daten der Datei als InputStream zur Verfügung.

Natürlich kann man sich auch alle Inhalte der file-Collection ausgeben lassen, um auf die entsprechenden Daten per InputStream zugreifen:

GridFS loadedGridFS = new GridFS(db);
DBCursor fileCursor = loadedGridFS.getFileList();
while (fileCursor.hasNext()) {
DBObject fileObject = fileCursor.next();
GridFSDBFile file = loadedGridFS.find((ObjectId) fileObject.get(„_id“));
InputStream in = file.getInputStream();
}

Nach erfolgreicher Arbeit sollte man nicht vergessen die Verbindung zur MongoDB zu schließen und damit die Connection zurück in den Pool zu legen:

mongo.close();