Geschrieben am

MongoDB zu nutzen erfordert MongoDB zu denken; Meteor Live-Queries

mongopain-01

Nehmen wir mal an, du möchtest eine Webapp entwickeln.

Im Zuge dessen würdest du dich auch für eine Datenbank entscheiden – und möglicherweise fiele deine Wahl auf MongoDB. Gründe für deine Wahl könnten sein:

  • es ist populär, hat eine große Community und eine gute Dokumentation um dir aus der Patsche zu helfen
  • es ist Dokumenten-basierend, sodass du dein Datenmodell „in einem Stück“ lesen und schreiben kannst
  • es unterstützt deine natürliche Denkweise im Bezug auf dein Datenmodell
  • seine API ist einfach zu benutzen
  • du benutzt Meteor
  • du nutzt es, um abstrakte Suchanfragen auszuführen, z.B. das Volumen von in der Datenbank abgeleten 3D-Geometrien

Und diese Gründe haben den Gründen, MongoDB nicht zu benutzen, sagen wir mal, überwogen:

  • es unterstützt keine Join-Operationen

Was dir dann unter Umständen passieren könnte ist (so wie es mir passiert ist), dass du dein Datenmodell so strukturierst, dass es (scheinbar) nicht mehr ohne Joint-Operationen auskommt, ohne Joint-Operationen komplett zu scheitern droht und alles, was du je wolltest, Joint-Operationen sind. Wenn dir das passiert, wird dir klar, dass die Annahme, dass deine natürliche, Dokumenten-basierende Denkweise nicht auch bedeutet, dass du „MongoDB“ denkst. Denn das wäre leider völlig falsch.

Du willst viele-zu-viele?

mongopain-01

Mama, wo sind meine Joins?

Die Tatsachen, dass viele-zu-viele Verbindungen in Datenbanken mit SQL joins gehandhabt werden können, und dass MongoDB keine SQL joins unterstützt (und wohl nie unterstützen wird), sind wohl die Auslöser vieler Artikel da draußen mit Aussagen wie „benutze niemals MongoDB“ oder „MongoDB ist #@!**$“ (zu viele um sie hier zu verlinken). In meinen Augen nutzen die Verfasser dieser Artikel MongoDB einfach auf die falsche Weise – oder aber für die falschen Zwecke, denn so oder so gibt es Anwendungszwecke, die besser mit NoSQL gelöst werden, und solche, die eher SQL erfordern. Dieser Artikel soll lediglich den NoSQL-Weg ebnen.
Zunächst ein kleines Beispiel einer Mailbox-Implementierung in einer Art sozialen Webapp. Da wären also:

  • eine Collection, die für jeden registrierten Benutzer dieser speziellen Webapp ein Dokument enthält. Eines dieser Dokumente könnte wie folgt aussehen:

  • eine Collection, die ein Dokument für jede Nachricht, die von einem Nutzer an einen (oder mehrere) andere gesendet wurde, enthält. Angenommen, du wolltest die Nutzerdaten in den Nachrichten verknüpfen (z.B. um mit Namensänderungen, geänderten Profilbildern o.ä. umzugehen), dann würden diese Dokumente so aussehen: (Hinweis: dieser Weg ist falsch und führt direkt in die Mongo-Hölle)

Im Beispiel oben sind der Verfasser (von) und die Empfänger (an) der Nachricht anhand ihrer ID verknüpft. Immer, wenn jemand die Nachricht abruft, würden wir eine Anfrage an die „Nachrichten“-Collection und mehrere Anfragen an die „Benutzer“-Collection stellen, um die erhaltenen Daten letzendlich zu ein Schnippsel HTML wie diesem hier zusammenzufügen:

Datum: Friday, January 10 2014 at 14:40
Von: Dan Jackson
An: Banana Joe, Iron Man
Betreff: Treffen morgen
Ich wollte euch nur kurz an unser Treffen morgen um 14:00 erinnern!

Und jedes Mal, wenn du eine Volltextsuche innerhalb der „Nachrichten“-Collection durchführen möchtest, müsstest du erstmal eine Volltextsuche in den verknüpften Collections durchführen (also der „Benutzer“-Collection) um im Anschluss eine abgleichende Suche in der Nachrichten-Collection durchzuführen. An diesem Punkt sollte dir auffallen, dass das Verlinken von Dokumenten anhand ihrer ID ungünstig sein kann (oder warum du „niemals MongoDB benutzen soltest“). Der Grund, warum das Verlinken so ungünstig ist, liegt in der MongDB-Philosophie, Datenmodelle „wie-sie-sind“ abzuspeichern und abzurufen. Nun sind aber verlinkte IDs überhaupt nicht „wie-sie-sind“, solange deine Freunde nicht in Form von 17-stelligen alphanumerischen Strings bei dir auftauchen, sondern mit Namen und Gesichtern (respektive „Avataren“).

erkenne deine Daten „wie-sie-sind“, bzw. „wie-sie-von-Bedeutung-sind“, und nicht „wie-sie-verknüpft-sind“

An dieser Stelle fangen wir an „MongoDB zu denken“, und fangen an, unsere Daten „wie-sie-von-Bedeutung-sind“ zur Verfügung zu stellen. Das ist einfacher, als viele glauben, und es wird noch einfacher, wenn man ein Framework wie Meteor nutzt, das sogenannte Live-Queries bereitstellt.
Zunächst bringen wir unser Datenmodell in eine Form, in der es abbildet, was für uns von Bedeutung ist. Wenn du genau hinsiehst wirst du dich fragen, warum die ID im Datenmodell noch vorhanden ist, obwohl sie ja für die Nachricht selbst nicht von Bedeutung ist – wir werden sie später benötigen, wenn wir die kopierten Daten mit ihren Originalen synchronisieren.

Alles, was wir jetzt noch tun müssen, ist, die Collections zu synchronisieren, da wir ja Daten aus der „Benutzer“-Collection in die „Nachrichten“-Collection kopiert haben. Das ist weniger komplex als es scheint. Die folgende Meteor Live-Query synchronisiert die beiden Collections permanent (in der Richtung Nutzer -> Nachrichten natürlich).

Hier ist syncUserMessages unsere Live-Query, die unsere Collections bis in alle Ewigkeit (oder bis stop() an ihr aufgerufen wird) synchronisiert. Immer, wenn ein Nutzer (oder ein beliebiger Prozess) eines der Felder in der Query („profil.vorname“, „profil.nachname“ oder „profil.avatar“) modifiziert, werden alle Nachrichten, die der Nutzer je gesendet oder empfangen hat abgeglichen. Diese einfache Methode ist effizienter als sie aussieht – für MongoDB ist es einfach, diese Dokumente zu aktualisieren, und für die Live-Query ist es einfach, die Synchronisation abzuwicken, da sie seit Meteor Version 0.7.0.1 über das Oplog tailing läuft. Vor allen Dingen ist der Regelfall aber der Datenabruf, der auf diesem Wege enorm verschlankt wird. Diese Methode funktioniert gut, solange nicht beide der Folgenden Fälle gemeinsam eintreten:

  • verlinkte Daten ändern sich sehr, sehr häufig
  • Ein Dokument enthält sehr viele Links oder ist sehr, sehr häufig verlinkt

Wenn beides zusammenkommt, dann haben wir es in den meisten Fällen mit handfesten „viele-zu-viele“-Verknüpfungen in der Datenbank zu tun, und für all diese Fälle, wird die Methode immer noch funktionieren, solange die Synchronisation in einem verträglichen Intervall, z.B. einmal täglich, durchgeführt wird. z.B. in obigem Beispiel: Es ist nicht wirklich von Bedeutung, ob eine Namensänderung sich sofort in der Nachrichtendatenbank widerspiegelt.

Keine Verknüpfungen != kein SQL

Falls das nicht all deinen Anforderungen genügt, kannst du immer noch IDs verknüpfen. Es gibt ein paar wenige Fälle, in denen das Sinn macht, aber viel mehr Fälle, in denen ich davon abraten würde. Ein typischer Fall, bei dem ich ID-Verknüpfen bevorzugen würde, wäre ein Online-Indikator, der dem Empfänger der Nachricht (obiges Beispiel) mitteilt, ob der Absender noch online ist. Ich würde nicht hunderte von Nachrichten aktualisieren wollen, nur weil ein Nutzer online geht. Ohnehin sollte der kopierte Datensatz immer die Quell-ID enthalten (zur synchronisation), diese kann auch als direkte Verknüpfung genutzt werden.

Brauche Joins = brauche SQL

Abschließend sei gesagt, dass es natürlich auch Fälle gibt, in denen das Leben ohne SQL joins wirklich hart ist. In solchen Fällen ist man selbstredend mit einer SQL Datenbank besser bedient. Dort warten andere Schwierigkeiten, wie Performance-Abwägungen beim Normalisieren/Denormalisieren von Daten, Echtzeit-Daten, komplexe Joint-Strukturen, und vieles mehr.
Ich hoffe jedenfalls, die hier gezeigte Strategie erspart euch ein paar Nerven!

Schreibe einen Kommentar

Deine E-Mail-Adresse wird nicht veröffentlicht. Erforderliche Felder sind markiert *