Beziehungslos mit Plan

Datenmodellierung in nicht relationalen Datenbanken
Kommentare

„Erstelle ein korrekt normalisiertes Datenbankschema!“ lautet eine beliebte Übungsaufgabe für angehende Informatiker. Das Erkennen und Beseitigen von Redundanzen geht irgendwann ins Blut über und wird alltägliches Handwerkszeug. Doch was ist zu beachten, wenn die Datenbank nicht relational ist, sondern aus dem Kanon der „neuen“, nicht relationalen Datenbanken à la MongoDB, CouchDB, Riak oder Redis stammt?

Längst sind nicht relationale Datenbanken, häufig etwas nichtssagend als „NoSQL“-(Not-only-SQL-)Datenbanken bezeichnet, im technologischen Mainstream und in Real-Life-Projekten angekommen. Systemarchitekten ersetzen das relationale Datenbanksystem komplett durch einen der Newcomer oder nehmen für Spezialaufgaben Vertreter dieser Spezies ins Architekturportfolio auf.

Für Entwickler bedeutet das ein Umdenken bei der Modellierung ihrer Persistenzschicht. Die Modellierung in relationalen Datenbanken beruht auf der Erstellung eines normalisierten Relationenmodells mit Tupeln und Relationen, die sich dann nahezu direkt in ein Tabellensystem umwandeln lassen. Im Vordergrund bei der Modellierung steht die Frage: „Wie sind meine Daten strukturiert und welche Antworten habe ich in meinen Daten?“ Beim Modellieren für NoSQL-Datenbanken steht typischerweise die Frage im Mittelpunkt: „Wie will ich auf die Daten zugreifen, welche Fragen an die Daten habe ich?“, verbunden mit: „Welche Möglichkeiten bietet die Datenbank, die Daten zu strukturieren und abzufragen?“

Schöne, bunte NoSQL-Welt

Bei relationalen Datenbanken erfolgt der Zugriff auf Daten über ein im Großen und Ganzen gemeinsames Datenmodell, das auf Tabellen mit Zeilen und strikt typisierten Spalten beruht. SQL ist als halbwegs standardisierte Abfragesprache und Zugriffsmöglichkeit etabliert.

Die NoSQL-Welt ist deutlich heterogener: Mittlerweile existieren etwa 150 NoSQL-Datenbanken am Markt [1], die die unterschiedlichsten Ansätze verfolgen: Einige fokussieren sich auf die horizontale Skalierbarkeit. Andere betonen die flexiblen Abfragemöglichkeiten. Wieder andere wollen jede Aufgabe als Graphenproblem betrachten und lösen.

NoSQL-Datenbanken speichern also ganz unterschiedliche Datenstrukturen: Key/Value, Dokumente, Graphen usw. Standardisierungsbemühungen im Bereich Abfragen, wie z. B. UNQL oder JSONiq als interoperable Abfragesprachen, waren bisher nicht erfolgreich bzw. stehen erst am Anfang. Das ist angesichts der heterogenen Ansätze auch nicht sonderlich erstaunlich.

NoSQL-Datenmodelle im Überblick

Die folgende Einteilung gibt einen ersten Überblick, welche Datenbankmodelle sich häufig in der NoSQL-Welt finden:

Simple Key-Value-Stores stellen einen einfachen Datenbanktyp dar. Sie bilden Schlüssel (Keys) auf Werte (Values) ab. Die Schlüssel sind eindeutig und damit das Äquivalent zu einem Primary Key in einer relationalen Datenbank. Die Werte selbst sind für simple Key-Value-Stores meist „opaque“: Sie werden als binäre Daten ohne Kenntnis der darin gegebenenfalls enthaltenen Strukturen abgespeichert.

Eine Abfrage der gespeicherten Werte ist in simplen Key-Value-Stores nur über den jeweiligen Schlüssel möglich. Eine direkte Suche nach Werten ist nicht möglich, da der Key-Value-Store die Werte nicht interpretieren kann. Häufig ist es auch nicht möglich, in einer vorhersagbaren Reihenfolge über alle Key-Value-Paare zu iterieren. Bereichsabfragen auf Schlüsseln (z. B. von Schlüssel „user-001“ bis „user-008“ oder „project-001-*“) werden somit oft nicht unterstützt. Die Unabhängigkeit der Schlüssel voneinander macht solche Key-Value-Stores effizient und relativ simpel implementierbar. Allerdings ist die Abfragemöglichkeit für Entwickler auch sehr eingeschränkt und erfordert Disziplin bei der Vergabe der Schlüsselnamen. Memcached ist ein Prototyp eines solchen simplen Key-Value-Stores.

Als Erweiterung des simplen Modells bieten einige Key-Value-Stores an, den globalen Namensraum in verschiedene Keyspaces (auch Segmente oder Buckets genannt) aufzuteilen, die dann separat verwaltet und abgefragt werden können. Schlüsselnamen sind hierbei nicht mehr global eindeutig, sondern nur noch in Kombination mit dem jeweiligen Keyspace. Diese zweistufige Hierarchie erweitert die Abfragemöglichkeiten bereits deutlich.

Mittlerweile erlauben viele Key-Value-Stores auch die Indizierung von Werten. Sie bieten damit neben dem herkömmlichen Zugriff per Schlüssel auch die Möglichkeit, gesuchte Werte schnell über Sekundär-Indizes abzufragen. Bei Verwendung sortierter Indizes sind damit auch Bereichsabfragen auf Werte möglich. Diese Möglichkeiten bestehen z. B. in Riak und Amazons DynamoDB-Service. Da der Store die zu indizierenden Teile von Werten mangels Kenntnis der internen Strukturen nicht ermitteln kann, muss der Entwickler der Datenbank den in den Index aufzunehmenden Wert selbst mitteilen.

Extended-Column Stores, oft auch Wide-Column oder Column-Family Stores genannt, basieren ebenfalls auf dem Konzept der Key-Value-Stores. Die Daten werden grundsätzlich in einer mehrstufigen Schlüsselhierarchie abgelegt. Die in Keyspaces organisierten Schlüssel enthalten nun „Columns“ genannte Unterelemente. Diese Columns enthalten entweder direkt die eigentlichen Werte oder enthalten als „Supercolumns“ wiederum selbst Unter-Columns.

Der Begriff „Column“ entspricht hierbei nicht dem einer Spalte wie in relationalen Datenbanken. Die Ablage in Extended-Column Stores ähnelt eher einem verschachtelten assoziativen Array. Die oberen Ebenen der Hierarchie müssen meist vorab definiert werden und gelten für alle Datensätze. Die unteren Hierarchieelemente sind meist dynamisch und je Datensatz unterschiedlich definierbar, ohne dass dies wie in einer relationalen Datenbank zu Problemen führt.

Da alle Hierarchieelemente benannt und über ihre Namen ansprechbar sind, lassen sich hiermit Abfragen auf Elemente der untersten Ebene einfach und effizient ausführen. Bereichsabfragen sind möglich. Da die Werte in der Regel typenlos sind und vom Store nicht weiter interpretiert werden können, erfolgen die Bereichsabfragen allerdings nicht über Werte-, sondern über Schlüssel-Bereiche.

In der Praxis werden die Daten in Extended-Column Stores auf mehrere Server verteilt. Bereichsabfragen sind dann nur effizient, wenn zusammen abgefragte Daten auf demselben Server abgelegt werden. Dies erfordert oft Wissen über den Aufbau der Schlüsselhierarchie und bedingt die manuelle Definition der Verteilungslogik. Apache Cassandra als bekannter Vertreter der Extended-Column Stores erlaubt beispielsweise eine solche durch Anwender konfigurierbare Partitionierung.

Dokumentendatenbanken speichern Daten in Form von Dokumenten, häufig nach außen als JSON repräsentiert. Dokumente sind Aggregate mit strukturierten, typisierten Attributen. Ein Dokument kann Listen enthalten und Unterdokumente einbetten, lässt damit also eine hierarchische Organisation von Daten zu. Dokumente aus derselben Domäne werden in der Regel in so genannten „Collections“ organisiert.

Dokumente sind wie in Key-Value-Stores über eindeutige Schlüssel identifiziert und dementsprechend über ihre Schlüssel abfragbar. Da die Dokumentattribute typisiert sind, ermöglichen Dokumentendatenbanken darüber hinaus vielfältige weitere Abfragemöglichkeiten, z. B. die Suche nach Dokumenten mit bestimmten Attributen oder bestimmten Werten. Hier sind deutliche komplexere Abfragen möglich als in Stores, die die gespeicherten Daten nicht interpretieren können. Bekannte Dokumentendatenbanken sind MongoDB und CouchDB.

Graphendatenbanken schließlich speichern einerseits Knoten und andererseits Kanten (Verbindungen) zwischen diesen Knoten. Die Gesamtheit der Knoten und Kanten wird als „Graph“ bezeichnet. Im Graph eines sozialen Netzwerks könnten die Knoten z. B. Personen und die Kanten die Beziehungen zwischen diesen Personen repräsentieren. Kanten können eigene Attribute besitzen, die die Verbindung genauer beschreiben, beispielsweise den Typ der Beziehung zwischen zwei Personen. Die eigenen Daten von Knoten und Kanten werden in Graphdatenbanken oftmals in Form von Dokumenten gespeichert. Die Information darüber, welche Knoten mit anderen verbunden sind, wird zusätzlich in speziellen Datenstrukturen gespeichert, mit denen die Beziehungen schnell wieder abgefragt werden können. Dies ermöglicht ein schnelles Traversieren von Graphen bei der Suche nach direkten (z. B. „wer ist mit Person A befreundet?“) und indirekten („welche Personen sind mit Freunden von Person A befreundet?“) Beziehungen unterschiedlicher Komplexität. Ein verbreiteter Vertreter dieses Genres ist Neo4j.

Darüber hinaus existieren Datenbanken, die Eigenschaften von Key-Value-Stores und Dokumentendatenbanken miteinander mixen: In Redis, einem Open Source Key-Value-Store, gibt es unter anderem die Datentypen Listen, Sets und Hashes (vergleichbar mit assoziativen Arrays in PHP), mit denen Werte einen Typ und eine interne Struktur erhalten. Man kann in einem Set zum Beispiel prüfen, ob ein Element existiert, einzelne Elemente können hinzugefügt oder gelöscht werden usw. Nicht ohne Grund nennt sich Redis auch „Datastructure Server“.

Auch „Multi-Model“-Datenbanken versuchen, Features aus mehreren Kategorien, z. B. Key-Value-Store, Dokumenten- und Graphdatenbanken, unter einem Dach zu vereinen und Anwendern eine ausgewogene Mischung von Features in einem Produkt zu bieten. Vertreter aus diesem Bereich sind beispielsweise OrientDB und ArangoDB.

Die Qual der Wahl oder „Welches Schweinderl hätten S‘ denn gerne?“

Die verschiedenen Kategorien von NoSQL-Datenbanken sind unterschiedlich gut für verschiedene Einsatzbereiche geeignet. Zudem setzt jedes Produkt innerhalb einer Kategorie eigene Schwerpunkte und hat bestimmte Stärken und Schwächen. Die erste Frage bei der Auswahl einer nicht relationalen Datenbank ist daher: Welche Kategorie und welches Produkt deckt die Projektanforderungen am besten ab, passt am besten zum Einsatzszenario?

Typische Einsatzszenarien für Key-Value-Stores sind Problemstellungen mit Fokus auf höchster Performance und/oder extremer Skalierbarkeit. Extended-Column Stores werden häufig verwendet, wenn die Daten eine gewisse Hierarchie besitzen, auf deren Einzelattribute gesondert zugegriffen werden soll. Dokumentendatenbanken bieten als „All-Purpose“-Datenbanken einen ausgewogenen Featuremix. Graphendatenbanken ermöglichen darüber hinaus die Modellierung und Abfrage komplexer Beziehungen.

Die Unterschiedlichkeit der verschiedenen nicht relationalen Datenbanksysteme führt zur Idee der „polyglotten Persistenz“. Damit ist gemeint, dass man in einer umfangreicheren Anwendung nicht nur eine, sondern mehrere Datenbanken parallel einsetzt. Ausgewählt wird jeweils die Datenbank, die „best choice“ für einen bestimmten Einsatzzweck ist. Voraussetzung dafür ist, dass die Anwendung nicht die Datenbank als verbindendes Element zwischen den Komponenten nutzt, sondern die Komponenten in einer serviceorientierten Architektur lose gekoppelt sind. Selbstverständlich kann auch eine relationale Datenbank in einer solchen Architektur ihren Platz finden, denn relationale Datenbanken sind trotz des Aufkommens von NoSQL-Datenbanken natürlich weiterhin gut und sinnvoll in den Bereichen einsetzbar, in denen sie ihre spezifischen Stärken ausspielen können.

„Polyglotte Persistenz“ führt im Idealfall dazu, die Stärken der verschiedenen Datenbanken zu nutzen und die Schwächen zu vermeiden. Sie verlangt allerdings nicht nur einen Überblick über die vorhandenen Produkte mit ihren Stärken und Schwächen, sondern auch die Bereitschaft aller Beteiligten, verschiedene Technologien zu erlernen, zu betreiben und zu warten. In der Praxis wird man sich daher auf wenige, für den jeweiligen Einsatzzweck besonders geeignete Datenbanken beschränken wollen.

Warum überhaupt modellieren?

NoSQL-Datenbanken werden oft als „schemafrei“ bezeichnet, weil im Vorfeld keine Struktur der Daten definiert werden muss. Frei von Modellierungsentscheidungen ist man als Entwickler aber nicht.

In praktisch allen NoSQL-Datenbanken sind im Hintergrund Schemaelemente am Werk, selbst in den an sich einfach strukturierten Key-Value-Stores. Einige bieten Keyspaces und Sekundär-Indizes. Diese können eingesetzt werden, um die Schlüssel in Bereiche einzuteilen und schnellere Abfragen auf Werte und Wertebereiche zu ermöglichen.

Darüber hinaus haben die gespeicherten Daten natürlich eine Struktur. Das damit implizit definierte Datenmodell hat vielfältige Auswirkungen auf die Anwendung:

Das Datenmodell bestimmt die Performanz beim Lesen und Schreiben der Daten. Es beeinflusst außerdem, wie viel Speicherplatz die Datenbank benötigt, gerade in Cloud-Umgebungen oder bei sehr großen Datenbanken ein nicht zu unterschätzender Aspekt. Ganz wichtig: Durch das Datenmodell legt man viel stärker als in relationalen Datenbanken fest, welche Abfragen überhaupt möglich sind, welche besonders performant ausgeführt werden und welche auf Applikationsseite substituiert werden müssen. Last but not least vereinfacht ein cleveres Datenmodell es auch, die Daten konsistent zu halten, auch in einem Cluster mit vielen Servern und eventuellen Systemausfällen.

Wie in relationalen Datenbanken ist es auch in der nicht relationalen Welt nicht einfach, ein „gutes“ Datenmodell zu finden, das die Anforderungen Performanz, Kompaktheit, Abfragekomfort und Konsistenz unter einen Hut bringt. Trotzdem ist eine Datenmodellierung auch bei Nutzung von NoSQL-Datenbanken wichtig und sollte nicht unterlassen werden.

Aggregate statt Relationen

Noch einmal kurz zurück zu relationalen Datenbanken: Datenmodellierung in relationalen Datenbanken orientiert sich am Prinzip der Normalisierung: Die Datenattribute werden schrittweise zerlegt in mehrere Relationen, indem die verschiedenen Normalisierungsregeln angewendet werden. Am Ende sind alle Informationen so in Tabellenspalten (Attribute) und Tabellen (Relationen) aufgeteilt, dass keine vermeidbaren Redundanzen mehr vorhanden sind.

Key-Value-Stores und Dokumentendatenbanken sind „Aggregat-orientiert“. Der Begriff kommt eigentlich aus dem Domain-driven Design und wurde von Martin Fowler für NoSQL-Datenbanken übernommen: Ein Aggregat ist demnach eine „collection of related objects that we wish to treat as a unit“, zum Beispiel ein Kunde mit seinen Bestellungen.

Schneller, höher, weiter – Skalierbarkeit durch De-Normalisierung

Redundanzen und Denormalisierung sind in diesem Modell gewollt. Warum? Aggregate werden immer als Gesamtheit behandelt, d. h. vollständig (atomar) gespeichert oder gelesen, auch wenn nur Teilaspekte benötigt werden. Somit können innerhalb eines Aggregats keine Konsistenzprobleme auftreten. Verschiedene Aggregate sind unabhängig voneinander und können somit unabhängig verwaltet werden. In vielen nicht relationalen Datenbanken ist dies die Grundlage der horizontalen Skalierbarkeit: Wird mehr Durchsatz benötigt oder steigt die Datenmenge, können im Idealfall einfach zusätzliche Server in den Datenbank-Cluster aufgenommen werden. Die Datenbank kann den Datenbestand so auf die Server im Cluster verteilen, dass eine möglichst gleichmäßige Auslastung erreicht wird. Gleichzeitig wird durch redundante Speicherung im Cluster erreicht, dass bei einem Ausfall einzelner Server andere Server an ihrer Stelle einspringen können, und somit bleibt ein Weiterbetrieb mit Zugriff auf den kompletten Datenbestand möglich. Da immer komplette Aggregate transferiert werden, sind die Daten eines Aggregats in sich immer gültig. Müsste man sich dagegen die Einzelbestandteile von mehreren Orten (z. B. verschiedenen Servern) zusammensuchen, entstünden dagegen leicht Konsistenzprobleme und außerdem höherer Overhead beim Abfragen.

Durch die Verteilung der Daten im Cluster können allerdings Konsistenzprobleme entstehen, die im Ein-Server-Betrieb nicht vorkommen: Wird beispielsweise ein Datum auf mehreren Servern gleichzeitig und unabhängig voneinander geändert, entsteht ein Konflikt, da nun zwei gültige Aggregate existieren. Ebenso ist es möglich, dass ein Datum auf einem Server geändert, anschließend aber von einem anderen Server abgefragt wird. Wenn die Änderung auf diesem Server noch nicht angekommen ist, wird als Ergebnis der Abfrage ein zwar vollständiges, aber veraltetes Aggregat zurückgeliefert. Die Lösung solcher Probleme wird von vielen verteilten NoSQL-Datenbanken den Anwendern überlassen, die damit eine größere Kontrolle beim Datenzugriff erhalten, allerdings solche Inkonsistenzfälle auch selber behandeln müssen.

Beispiel: Modellierung eines Blogs

Als Praxisbeispiel für die Modellierung nehmen wir uns einen Klassiker vor: Wir modellieren einen Blog mit Benutzern, Postings, Kommentaren und Tags. In einer relationalen Datenbank würde man diesen Use Case zum Beispiel wie in Abbildung 1 gezeigt modellieren.

Abb. 1: Modellierung eines Blogs in einer relationalen Datenbank

Abb. 1: Modellierung eines Blogs in einer relationalen Datenbank

Ein wichtiges Ziel des relationalen Entwurfs ist es, Redundanzen zu vermeiden: Ein User kann beispielsweise Autor von mehreren Blogposts sein, wird aber mit seinen Daten nur einmal in der Datenbank vorgehalten, in der Tabelle user. Ein Blogpost kann mehrere Tags haben, und jeder Tag kann in mehreren Blogposts vorkommen. Diese m:n-Beziehung modellieren wir, indem wir in der Tabelle tag jeweils einen Eintrag für jeden Tag machen. Jeder Blogpost hat einen Eintrag in der Tabelle post. Die Tabelle post_tag führt die Informationen zusammen. Dort wird für jeden Tag für jeden Post ein Eintrag gemacht. Hat ein Blog also drei Tags, enthält die Tabelle post_tag – richtig – drei Einträge. Die Zusammenführung der normalisierten Daten aus den verschiedenen Tabellen erfolgt erst zur Laufzeit über Joins und Projektionen.

Die Aggregat-orientierte Variante setzen wir mit einer Dokumentendatenbank um. Zu klären ist zunächst, welche Abfragen die Anwendung machen können soll: Sehr häufig wird die Anwendung alle Informationen zu einem Post benötigen, um den Post als Ganzes, inklusive Autorinformation und Kommentaren, darzustellen. Außerdem ist eine Funktion geplant, mit der alle Posts eines Autors angezeigt werden sollen. Schließlich soll der Administrationsbereich des Blogs Login-geschützt sein. Ein Benutzer muss beim Login authentifiziert werden können.

Für dieses Szenario bietet es sich an, zwei Collections zu bilden: Posts und Users. Posts enthält alle Informationen zu einem Blogpost inklusive Tags, dem Namen des Autors und Kommentaren.

In JSON-Notation könnte ein einzelnes Post-Dokument wie in Listing 1 aussehen

Listing 1

{
  "id": 1,
  "author": "henry",
  "created": "2012-09-05T11:19:21.000Z",
  "title": "My brand new blog rulez",
  "body": "I wrote a poem for it:...",
  "tags": ["poetry", "general", "art"],
  "comments": [{
    "author": "birdie23",
    "created": "2012-09-05T11:23:15.000Z",
    "comment": "Lovely, man!"
  }, {
    "author": "tom",
    "created": "2012-09-06T08:15.21.000Z",
    "comment": "OMG!"
  }]
}

In der Collection Users werden die Benutzernamen mit Klartext-Namen sowie ihren Passwort-Hashes gespeichert. Ein Beispiel für ein Users-Dokument:

{
"name": "henry",
  "fullname": {
    "first": "Henrik",
    "last": "Helenius"
  },
  "password": "$1$6r6dhpex$thvqpqyccicdjlotwr842/"
}

Man sieht, dass die im relationalen Modell vorhandenen Verknüpfungen hier aufgelöst sind und ein Post-Dokument ein Aggregat mit eingebetteten Tags, Kommentaren und Autornamen ist.

Zur Anzeige eines Posts muss somit lediglich ein Dokument aus der Datenbank geholt werden, das bereits alle benötigten Informationen enthält. Die Datenbank benötigt dafür (hoffentlich) nur eine einzige Leseoperation. Voraussetzung ist natürlich, dass die Daten bereits in der aggregierten Form gespeichert wurden. Teure Joins, die viele NoSQL-Datenbanken übrigens erst gar nicht unterstützen, können so vermieden werden. Die Daten liegen bereits in der für den Lesezugriff optimalen Form vor und können im Idealfall von der Applikation direkt in der Form genutzt werden, in der sie aus der Datenbank kommen. Hierdurch entfällt zusätzlich Aufwand für das Entpacken und Interpretieren der Daten auf Clientseite.

In einer relationalen Datenbank würde man für diesen Fall z. B. einen Join einsetzen und die Daten aus den verschiedenen Tabellen zur Laufzeit zusammenführen (zur Ermittlung der Tags des Posts müssen mindestens die Tabellen tag und post_tag gejoint werden). Man müsste darüber hinaus mehrere einzelne Abfragen auf die verschiedenen Datenbestände (post, tag, comment) ausführen, da diese ja unterschiedliche Zeilenzahlen liefern können (1 Post, n Tags, m Kommentare).

Für die Liste aller Posts eines Autors selektiert man in der Dokumentendatenbank alle Dokumente, deren Attribut author den Wert henry hat. Analog funktioniert die Anzeige aller Posts mit dem Tag art. Hierfür können wie in relationalen Datenbanken Sekundärindizes verwendet werden. Bei der Authentifizierung wird auf die Collection Users zugegriffen und das entsprechende Dokument für den Nutzer ermittelt, der sich gerade einloggt. Hier würde man in Dokumenten- und relationaler Datenbank jeweils ähnlich vorgehen.

Joins und Referenzen in der NoSQL-Welt

Für Version 2 des Blogs wünscht sich der Product Owner unseres NoSQL-Blog-Projekts, dass der volle Name der Blog-Autoren angezeigt wird. Problem: Der volle Name ist allerdings nicht in den „Post“-Dokumenten vorhanden, sondern in den Dokumenten der Users-Collection. Es muss also über Aggregatgrenzen hinweg selektiert werden, und dies in der Regel ohne Zuhilfenahme von Joins.

Je nach Datenbank gibt es unterschiedliche Konzepte dafür: Eine einfache und durchaus gebräuchliche Lösung ist, das Zusammenführen der Informationen in der Applikation selbst vorzunehmen. Man selektiert zunächst den Post aus Posts, sucht dann in Users nach dem Dokument mit dem entsprechenden Autorennamen und kombiniert beide Ergebnisse im Applikationscode. Die Applikation muss in diesem Fall zwei (simple) Leseabfragen ausführen. Diese Möglichkeit funktioniert natürlich auch in relationalen Datenbanken.

Einige Datenbanken kennen „materialized views“. Diese entstehen als Ergebnis einer MapReduce-Operation. MapReduce ist, vereinfacht ausgedrückt, ein Konzept, bei dem der Entwickler Code schreibt, der in der Datenbank ausgeführt wird. MapReduce kann zu vielfältigen Zwecken eingesetzt werden, zum Beispiel um inhaltlich zusammengehörige Daten aus mehreren Collections abzufragen und in neuen Dokumenten zusammenzuführen. Diese Dokumente werden, je nach Datenbanksystem, entweder manuell, über Batch-Jobs oder mehr oder weniger automatisch von der Datenbank aktualisiert, wenn die View abgefragt wird. Hiermit könnten also ebenso die benötigten „Post-User“-Aggregate gebaut werden, vorausgesetzt, die verwendete Datenbank unterstützt dieses Konzept.

Referenzen sind ein weiteres Konzept zur Überwindung von Aggregat-Grenzen. Referenzen verknüpfen Dokumente über deren eindeutige IDs miteinander, ähnlich wie Referenzen auf Primärschlüssel in relationalen Datenbanken. Nicht alle Dokumentendatenbanken unterstützen Referenzen, und selbst wenn, wird damit nicht keine referenzielle Integrität wie in relationalen Datenbanken garantiert (dies würde der einfachen Verteilbarkeit und Unabhängigkeit der einzelnen Aggregate zuwider laufen). MongoDB ist eine der Datenbanken, die das Referenzkonzept umsetzt und es ermöglicht, die verknüpften Dokumente direkt in das Ergebnis von Abfragen einzubetten. Allerdings wird für die meisten Fälle eine manuelle Verknüpfung empfohlen, d. h. in unserem Fall das separate Abfragen von Posts und Users durch die Anwendung.

Wie sicherlich bemerkt, sind wir durch die gewählte Art der Modellierung zwar gut für Leseabfragen aufgestellt, haben uns das aber mit Datenredundanz erkauft. Änderungen von Benutzernamen müssen wir nun an mehreren Orten durchführen: in der Users-Collection, in den author-Attributen der Post-Dokumente und den darin eingebetteten comments-Attributen. Da solche Änderungen teuer sind und vermieden werden sollten, bietet es sich an, in der Applikation entweder keine Aktionen anzubieten, die die Werte irgendwelcher in Verknüpfungen benutzten Attribute ändern können, oder es wird eben eine Verknüpfung über Referenzen oder sonstige, vom Benutzer nicht veränderbare (immutable) Attribute durchgeführt. Grundsätzlich bleibt es aber dabei, dass die Redundanz Aktualisierungen der Daten schwieriger macht. Es muss also im Vorfeld abgewogen werden, ob die Anwendung Daten häufig aktualisieren muss oder ob das vermieden werden kann.

„Schemafreiheit“ und Migration

Relationale Datenbanken arbeiten mit fixen Tabellenstrukturen und Schemas. Wenn sich die Anwendung ändert und andere Daten als bisher gespeichert werden sollen, muss meist das Datenbankschema mit dem geänderten Anwendungscode synchronisiert werden – und zwar bevor der neue Anwendungscode verwendet wird. Das Datenbankschema kann durch ALTER TABLE-Befehle migriert werden. Je nach vorhandener Datenmenge dauert das Abarbeiten dieser Statements mehr oder weniger lange.

Dokumentendatenbanken dagegen funktionieren anders: Zwar müssen auch hier ggf. neue Collections angelegt, aber Änderungen an Dokumentstrukturen müssen nicht vorab bekannt gemacht werden. Damit ist es beispielsweise möglich, neu angelegten Post-Dokumenten ein weiteres Attribut fullname mitzugeben, in denen ab dann der für die Anzeige der Posts benötigte volle Autorenname gespeichert wird. Dieses Attribut kann zwar auch allen bisherigen Post-Dokumenten hinzugefügt werden, aber zwingend erforderlich ist dies nicht. Die Posts-Collection enthält dann eben Dokumente mit verschiedenen Strukturen und unterschiedlichen Attributen. Hierdurch vermeidet man die gegebenenfalls aufwändige und riskante Migration des bestehenden Datenbestands.

Alles super also ohne explizites Schema? Nicht ganz, denn in diesem Fall obliegt es der Anwendung, die verschiedenen Dokumentversionen zu unterscheiden und entsprechend zu reagieren. Diese Versionierung erfordert also zusätzlichen Anwendungscode. Als Entwickler hat man aber immerhin die Auswahl zwischen verschiedenen Evolutionsstrategien und kann diese auch kombinieren. Möglichkeit 1: Man migriert alle vorhandenen Dokumente beim Aufspielen der neuen Version analog zu einem ALTER TABLE. Möglichkeit 2: Im Anwendungscode wird sichergestellt, dass er alle denkbaren Versionen eines Dokuments handhaben kann. Fragt der Code ein „veraltetes“ Dokument ab, fügt er die fehlenden Attribute on the Fly hinzu und lässt die überflüssigen Attribute weg. Damit ist das Dokument „lazy“ migriert worden.

Ähnlich kann man vorgehen, wenn man Aggregate aus irgendwelchen Gründen neu aufteilen muss. Zunächst passt man den Anwendungscode so an, dass er mit Dokumenten in der alten wie auch der neuen Struktur umgehen kann. Über einen Hintergrund-Job werden anschließend die gerade nicht verwendeten Dokumente einzeln migriert. Das zeigt, dass man die Schema-Evolution auch in eigentlich „schemafreien“ Datenbanken planen sollte.

Über ein Attribut im Dokument kann die Version auch explizit gespeichert werden, was die Verarbeitung auf Applikationsseite vereinfachen kann. Damit ist es sehr einfach möglich, die noch nicht migrierten Dokumente zu finden und den Umfang noch ausstehender Migrationen abzuschätzen.

Validierung und Konsistenz

In relationalen Datenbanken wird über das definierte Schema zumindest eine simple Datenvalidierung der Spaltennamen und Datentypen und bei Verwendung von Referenzen auch ein Konsistenzcheck durchgeführt. Diese Funktionen werden von den meisten NoSQL-Datenbanken nicht übernommen. Sie müssen stattdessen im Applikationscode durchgeführt werden: Die Applikation muss sicherzustellen, dass valide Daten in der gewünschten Struktur und den gewünschten Datentypen in der Datenbank landen.

Eine in relationalen Datenbanken nicht vorhandene Falle lauert übrigens bei der Attributbenennung: Da verschiedene Dokumente ja grundsätzlich verschiedene Attribute besitzen können, speichern die meisten Dokumentendatenbanken die in einem Dokument verwendeten Attributnamen mit jedem Dokument ab. Lange Attributnamen benötigen hierbei natürlich mehr Speicherplatz als kurze Namen, was bei einer großen Anzahl von Dokumenten und Attributen bereits deutliche Auswirkungen haben kann. Unter diesem Gesichtspunkt ist der nichtssagende Attributname c dem sprechenden Attributnamen creator_of_blog_post vorzuziehen, was aber leider nicht die Lesbarkeit des Codes erhöht.

Einige Object Document Mapper bieten Mapping-Funktionen, die die im Code verwendeten „lesbaren“ Attributnamen auf ihre kurzen Varianten in der Datenbank mappen können. In der Praxis werden allerdings oft nur wenig verschiedene Versionen von Dokumenten verwendet, und Dokumente derselben Collection sind häufig strukturell ähnlich oder gleich aufgebaut. Ein Ansatz zur effizienteren Speicherung von Dokumenten liegt daher auch darin, die (sich häufig wiederholenden) Dokumentstrukturen separat von den eigentlichen Dokumentdaten zu speichern. Dokumente mit gleicher Struktur können dabei dieselben, nur einmal gespeicherten Strukturdaten verwenden, wodurch trotz Verwendung langer Attributnamen weniger Speicherplatz verwendet werden muss. ArangoDB bietet diese Speicherstrategie.

Fazit

Das Ergebnis einer Datenmodellierung muss in relationalen wie nicht relationalen Datenbanken unterschiedlichsten Anforderungen genügen. Neben der Abdeckung der funktionalen Anforderungen geht es um die Sicherstellung von Performanz, Skalierbarkeit, Verfügbarkeit und Ausfallsicherheit. Last but not least müssen das Datenmodell und seine Migration zum Entwicklungsprozess passen.

Auch bei NoSQL-Datenbanken ist es wichtig, sich von Anfang an Gedanken über die Speicherung und Abfrage der Daten sowie ihre Migration zu machen. Hierbei müssen einige Entscheidungen getroffen werden, die man in der relationalen Welt nicht treffen kann oder muss. Eine sinnvolle Nutzung von NoSQL-Datenbanken setzt ebenfalls voraus, dass der Applikationscode mit den Besonderheiten dieser Datenbankgattung umgehen kann und selbst Funktionen übernimmt, die in relationalen Datenbanken noch von der Datenbank übernommen wurden.

Unsere Redaktion empfiehlt:

Relevante Beiträge

Meinungen zu diesem Beitrag

X
- Gib Deinen Standort ein -
- or -