Prinzipien zur Modellierung von CouchDB-Dokumenten

120

Ich habe eine Frage, die ich seit einiger Zeit zu beantworten versuche, aber nicht herausfinden kann:

Wie entwerfen oder teilen Sie CouchDB-Dokumente auf?

Nehmen Sie zum Beispiel einen Blog-Beitrag.

Der halb "relationale" Weg, dies zu tun, wäre, ein paar Objekte zu erstellen:

  • Post
  • Benutzer
  • Kommentar
  • Etikett
  • Snippet

Das macht sehr viel Sinn. Aber ich versuche, couchdb zu verwenden (aus all den Gründen, aus denen es großartig ist), um dasselbe zu modellieren, und es war äußerst schwierig.

Die meisten Blog-Beiträge geben Ihnen ein einfaches Beispiel dafür. Sie teilen es im Grunde auf die gleiche Weise auf, sagen aber, dass Sie jedem Dokument 'beliebige' Eigenschaften hinzufügen können, was auf jeden Fall schön ist. Sie hätten also so etwas in CouchDB:

  • Post (mit Tags und Snippets "Pseudo" -Modelle im Dokument)
  • Kommentar
  • Benutzer

Einige Leute würden sogar sagen, dass Sie den Kommentar und den Benutzer dort hineinwerfen könnten, also hätten Sie Folgendes:


post {
    id: 123412804910820
    title: "My Post"
    body: "Lots of Content"
    html: "<p>Lots of Content</p>"
    author: {
        name: "Lance"
        age: "23"
    }
    tags: ["sample", "post"]
    comments {
        comment {
            id: 93930414809
            body: "Interesting Post"
        } 
        comment {
            id: 19018301989
            body: "I agree"
        }
    }
}

Das sieht sehr gut aus und ist leicht zu verstehen. Ich verstehe auch, wie Sie Ansichten schreiben können, die nur die Kommentare aus all Ihren Post-Dokumenten extrahieren, um sie in Kommentarmodelle zu integrieren, genau wie Benutzer und Tags.

Aber dann denke ich: "Warum nicht einfach meine gesamte Website in einem einzigen Dokument zusammenfassen?":


site {
    domain: "www.blog.com"
    owner: "me"
    pages {
        page {
            title: "Blog"
            posts {
                post {
                    id: 123412804910820
                    title: "My Post"
                    body: "Lots of Content"
                    html: "<p>Lots of Content</p>"
                    author: {
                        name: "Lance"
                        age: "23"
                    }
                    tags: ["sample", "post"]
                    comments {
                        comment {
                            id: 93930414809
                            body: "Interesting Post"
                        } 
                        comment {
                            id: 19018301989
                            body: "I agree"
                        }
                    }
                }
                post {
                    id: 18091890192984
                    title: "Second Post"
                    ...
                }
            }
        }
    }
}

Sie könnten leicht Ansichten machen, um zu finden, was Sie damit wollten.

Dann ist die Frage, die ich habe, wie Sie bestimmen, wann Sie das Dokument in kleinere Dokumente aufteilen oder wann Sie "BEZIEHUNGEN" zwischen den Dokumenten herstellen sollen.

Ich denke, es wäre viel "objektorientierter" und einfacher, Wertobjekten zuzuordnen, wenn es so aufgeteilt würde:


posts {
    post {
        id: 123412804910820
        title: "My Post"
        body: "Lots of Content"
        html: "<p>Lots of Content</p>"
        author_id: "Lance1231"
        tags: ["sample", "post"]
    }
}
authors {
    author {
        id: "Lance1231"
        name: "Lance"
        age: "23"
    }
}
comments {
    comment {
        id: "comment1"
        body: "Interesting Post"
        post_id: 123412804910820
    } 
    comment {
        id: "comment2"
        body: "I agree"
        post_id: 123412804910820
    }
}

... aber dann sieht es eher aus wie eine relationale Datenbank. Und oft erbe ich etwas, das wie die "ganze Site in einem Dokument" aussieht, daher ist es schwieriger, es mit Beziehungen zu modellieren.

Ich habe viele Dinge darüber gelesen, wie / wann relationale Datenbanken im Vergleich zu Dokumentdatenbanken verwendet werden sollen, daher ist dies hier nicht das Hauptproblem. Ich frage mich nur, was eine gute Regel / ein gutes Prinzip ist, wenn Daten in CouchDB modelliert werden.

Ein weiteres Beispiel sind XML-Dateien / -Daten. Einige XML-Daten haben eine Tiefe von mehr als 10 Ebenen, und ich möchte dies mit demselben Client (z. B. Ajax on Rails oder Flex) visualisieren, mit dem ich JSON aus ActiveRecord, CouchRest oder einem anderen objektrelationalen Mapper rendern würde. Manchmal erhalte ich große XML-Dateien, die die gesamte Site-Struktur aufweisen, wie die folgende, und ich muss sie Value Objects zuordnen, um sie in meiner Rails-App zu verwenden, damit ich keine andere Methode zum Serialisieren / Deserialisieren von Daten schreiben muss ::


<pages>
    <page>
        <subPages>
            <subPage>
                <images>
                    <image>
                        <url/>
                    </image>
                </images>
            </subPage>
        </subPages>
    </page>
</pages>

Die allgemeinen CouchDB-Fragen lauten also:

  1. Welche Regeln / Prinzipien verwenden Sie, um Ihre Dokumente (Beziehungen usw.) aufzuteilen?
  2. Ist es in Ordnung, die gesamte Site in einem Dokument zusammenzufassen?
  3. Wenn ja, wie gehen Sie mit dem Serialisieren / Deserialisieren von Dokumenten mit beliebigen Tiefenstufen um (wie im obigen großen JSON-Beispiel oder im XML-Beispiel)?
  4. Oder verwandeln Sie sie nicht in VOs? Entscheiden Sie einfach, "diese sind zu verschachtelt für die objektrelationale Zuordnung, sodass ich nur mit XML / JSON-Rohmethoden darauf zugreifen kann"?

Vielen Dank für Ihre Hilfe. Die Frage, wie Sie Ihre Daten mit CouchDB aufteilen können, war für mich schwierig zu sagen: "So sollte ich es von nun an tun." Ich hoffe, bald dort zu sein.

Ich habe die folgenden Websites / Projekte studiert.

  1. Hierarchische Daten in CouchDB
  2. CouchDB Wiki
  3. Sofa - CouchDB App
  4. CouchDB Der endgültige Leitfaden
  5. PeepCode CouchDB Screencast
  6. CouchRest
  7. CouchDB README

... aber sie haben diese Frage immer noch nicht beantwortet.

Lance Pollard
quelle
2
wow du hast hier einen ganzen Aufsatz geschrieben ... :-)
Eero
8
Hey, das ist eine gute Frage
Elmarco

Antworten:

26

Es gab bereits einige gute Antworten darauf, aber ich wollte dem Mix der Optionen für die Arbeit mit der von viatropos beschriebenen ursprünglichen Situation einige neuere CouchDB-Funktionen hinzufügen.

Der entscheidende Punkt beim Aufteilen von Dokumenten ist, wo es zu Konflikten kommen kann (wie bereits erwähnt). Sie sollten niemals massiv "verwickelte" Dokumente in einem einzigen Dokument zusammenhalten, da Sie einen einzigen Revisionspfad für völlig unabhängige Aktualisierungen erhalten (z. B. Hinzufügen eines Kommentars zum Hinzufügen einer Revision zum gesamten Site-Dokument). Das Verwalten der Beziehungen oder Verbindungen zwischen verschiedenen, kleineren Dokumenten kann zunächst verwirrend sein, aber CouchDB bietet verschiedene Optionen zum Kombinieren unterschiedlicher Teile zu einzelnen Antworten.

Die erste große ist die Ansichtssortierung. Wenn Sie Schlüssel / Wert-Paare in die Ergebnisse einer Map / Reduce-Abfrage ausgeben, werden die Schlüssel basierend auf der UTF-8-Sortierung sortiert ("a" steht vor "b"). Sie können auch komplexe Schlüssel aus Ihrer Map ausgeben / als JSON-Arrays reduzieren : ["a", "b", "c"]. Auf diese Weise können Sie eine Art "Baum" einfügen, der aus Array-Schlüsseln besteht. Anhand Ihres obigen Beispiels können wir die post_id ausgeben, dann den Typ der Dinge, auf die wir verweisen, und dann ihre ID (falls erforderlich). Wenn wir dann die ID des referenzierten Dokuments in einem Objekt in dem zurückgegebenen Wert ausgeben, können wir den Abfrageparameter 'include_docs' verwenden, um diese Dokumente in die Map / Reduce-Ausgabe aufzunehmen:

{"rows":[
  {"key":["123412804910820", "post"], "value":null},
  {"key":["123412804910820", "author", "Lance1231"], "value":{"_id":"Lance1231"}},
  {"key":["123412804910820", "comment", "comment1"], "value":{"_id":"comment1"}},
  {"key":["123412804910820", "comment", "comment2"], "value":{"_id":"comment2"}}
]}

Wenn Sie dieselbe Ansicht mit '? Include_docs = true' anfordern, wird ein 'doc'-Schlüssel hinzugefügt, der entweder die' _id 'verwendet, auf die im' value'-Objekt verwiesen wird, oder wenn diese im 'value'-Objekt nicht vorhanden ist, wird sie verwendet die '_id' des Dokuments, aus dem die Zeile ausgegeben wurde (in diesem Fall das 'post'-Dokument). Bitte beachten Sie, dass diese Ergebnisse ein 'ID'-Feld enthalten, das auf das Quelldokument verweist, aus dem die Ausgabe erfolgt ist. Ich habe es aus Platz- und Lesbarkeitsgründen weggelassen.

Wir können dann die Parameter 'start_key' und 'end_key' verwenden, um die Ergebnisse auf die Daten eines einzelnen Posts zu filtern:

start_key = ["123412804910820"] & end_key = ["123412804910820", {}, {}]
Oder extrahieren Sie die Liste speziell für einen bestimmten Typ:
start_key = ["123412804910820", "comment"] & end_key = ["123412804910820", "comment", {}]
Diese Abfrageparameterkombinationen sind möglich, da sich ein leeres Objekt (" {}") immer am unteren Rand der Sortierung befindet und null oder "" immer am oberen Rand stehen.

Die zweite hilfreiche Ergänzung von CouchDB in diesen Situationen ist die Funktion _list. Auf diese Weise können Sie die oben genannten Ergebnisse über ein Template-System ausführen (wenn Sie HTML, XML, CSV oder was auch immer zurückhaben möchten) oder eine einheitliche JSON-Struktur ausgeben, wenn Sie den gesamten Inhalt eines Posts anfordern möchten (einschließlich Autoren- und Kommentardaten) mit einer einzigen Anforderung und als einzelnes JSON-Dokument zurückgegeben, das den Anforderungen Ihres clientseitigen / UI-Codes entspricht. Auf diese Weise können Sie das einheitliche Ausgabedokument des Beitrags folgendermaßen anfordern:

/ db / _design / app / _list / posts / unified ?? start_key = ["123412804910820"] & end_key = ["123412804910820", {}, {}] & include_docs = true
Ihre _list-Funktion (in diesem Fall "unified") würde die Ergebnisse der Ansichtszuordnung / -reduzierung (in diesem Fall "posts") verwenden und sie über eine JavaScript-Funktion ausführen, die die HTTP-Antwort in dem von Ihnen angegebenen Inhaltstyp zurücksendet brauchen (JSON, HTML, etc).

Wenn Sie diese Dinge kombinieren, können Sie Ihre Dokumente auf jeder Ebene aufteilen, die Sie für Aktualisierungen, Konflikte und Replikationen für nützlich und "sicher" halten, und sie dann nach Bedarf wieder zusammensetzen, wenn sie angefordert werden.

Hoffentlich hilft das.

BigBlueHat
quelle
2
Ich bin mir nicht sicher, ob dies Lance geholfen hat, aber ich weiß eines; es hat mir definitiv sehr geholfen! Das ist fantastisch!
Mark
17

Ich weiß, dass dies eine alte Frage ist, aber ich bin darauf gestoßen, um herauszufinden, wie genau dieses Problem am besten gelöst werden kann. Christopher Lenz hat einen schönen Blog-Beitrag über Methoden zur Modellierung von "Joins" in CouchDB geschrieben . Einer meiner Take-Aways war: "Die einzige Möglichkeit, das konfliktfreie Hinzufügen verwandter Daten zu ermöglichen, besteht darin, diese verwandten Daten in separaten Dokumenten abzulegen." Der Einfachheit halber möchten Sie sich also der "Denormalisierung" zuwenden. Unter bestimmten Umständen stoßen Sie jedoch aufgrund widersprüchlicher Schreibvorgänge auf eine natürliche Barriere.

Wenn in Ihrem Beispiel für Beiträge und Kommentare ein einzelner Beitrag und alle seine Kommentare in einem Dokument gespeichert sind, verursachen zwei Personen, die gleichzeitig versuchen, einen Kommentar zu veröffentlichen (dh gegen dieselbe Überarbeitung des Dokuments), einen Konflikt. Dies würde in Ihrem Szenario "Ganze Site in einem einzigen Dokument" noch schlimmer werden.

Ich denke also, die Faustregel wäre "denormalisieren, bis es weh tut", aber der Punkt, an dem es "weh tut", ist der Punkt, an dem Sie mit hoher Wahrscheinlichkeit mehrere Änderungen gegen dieselbe Revision eines Dokuments veröffentlichen.

jake l
quelle
Interessante Antwort. Vor diesem Hintergrund sollte man sich fragen, ob eine Website mit relativ hohem Datenaufkommen überhaupt alle Kommentare für einen einzelnen Blog-Beitrag in einem Dokument enthält. Wenn ich das richtig lese, bedeutet dies, dass Sie jedes Mal, wenn Sie Leute haben, die schnell hintereinander Kommentare hinzufügen, möglicherweise Konflikte lösen müssen. Natürlich weiß ich nicht, wie schnell sie nacheinander sein müssten, um dies auszulösen.
pc1oad1etter
1
In dem Fall, in dem Kommentare Teil des Dokuments in Couch sind, können gleichzeitige Kommentarbeiträge zu Konflikten führen, da Ihr Versionsumfang der "Beitrag" mit all seinen Kommentaren ist. In dem Fall, dass jedes Ihrer Objekte eine Sammlung von Dokumenten ist, werden diese einfach zu zwei neuen "Kommentar" -Dokumenten mit Links zurück zum Beitrag und ohne Bedenken hinsichtlich einer Kollision. Ich möchte auch darauf hinweisen, dass das Erstellen von Ansichten zum "objektorientierten" Dokumentendesign einfach ist - Sie geben beispielsweise den Schlüssel eines Beitrags ein und geben dann alle nach einer Methode sortierten Kommentare für diesen Beitrag aus.
Riyad Kalla
16

Das Buch heißt es, wenn ich mich richtig erinnere, zu denormalisieren, bis "es weh tut", wobei die Häufigkeit zu berücksichtigen ist, mit der Ihre Dokumente möglicherweise aktualisiert werden.

  1. Welche Regeln / Prinzipien verwenden Sie, um Ihre Dokumente (Beziehungen usw.) aufzuteilen?

Als Faustregel füge ich alle Daten hinzu, die zum Anzeigen einer Seite bezüglich des betreffenden Elements erforderlich sind. Mit anderen Worten, alles, was Sie auf ein Stück Papier drucken würden, das Sie jemandem übergeben würden. Beispielsweise würde ein Börsenkursdokument zusätzlich zu den Nummern den Namen des Unternehmens, die Börse und die Währung enthalten. Ein Vertragsdokument würde die Namen und Adressen der Gegenparteien sowie alle Informationen zu Daten und Unterzeichnern enthalten. Aktienkurse von unterschiedlichen Daten würden jedoch separate Dokumente bilden, separate Verträge würden separate Dokumente bilden.

  1. Ist es in Ordnung, die gesamte Site in einem Dokument zusammenzufassen?

Nein, das wäre dumm, weil:

  • Sie müssten bei jedem Update die gesamte Site (das Dokument) lesen und schreiben, und das ist sehr ineffizient.
  • Sie würden von keinem View-Caching profitieren.
Eero
quelle
3
Danke, dass du dich ein bisschen mit mir beschäftigt hast. Ich habe die Idee, "alle Daten einzuschließen, die zum Anzeigen einer Seite in Bezug auf das betreffende Element erforderlich sind", aber das ist immer noch sehr schwierig zu implementieren. Eine "Seite" kann eine Seite mit Kommentaren, eine Seite mit Benutzern, eine Seite mit Beiträgen oder eine Seite mit Kommentaren und Beiträgen usw. sein. Wie würden Sie sie dann hauptsächlich aufteilen? Sie können Ihren Vertrag auch mit Benutzern anzeigen lassen. Ich bekomme die "formularartigen" Dokumente, die sinnvoll sind, um sie getrennt zu halten.
Lance Pollard
6

Ich denke, Jakes Antwort ist einer der wichtigsten Aspekte der Arbeit mit CouchDB, die Ihnen bei der Entscheidung über den Umfang helfen können: Konflikte.

In dem Fall, in dem Sie Kommentare als Array-Eigenschaft des Posts selbst haben und nur eine 'Post'-Datenbank mit einer Reihe riesiger' Post'-Dokumente haben, wie Jake und andere richtig betont haben, können Sie sich ein Szenario vorstellen Ein sehr beliebter Blog-Beitrag, bei dem zwei Benutzer gleichzeitig Änderungen am Beitragsdokument einreichen, was zu einer Kollision und einem Versionskonflikt für dieses Dokument führt.

ASIDE: Wie in diesem Artikel erwähnt, sollten Sie auch berücksichtigen, dass Sie jedes Mal, wenn Sie dieses Dokument anfordern / aktualisieren, das Dokument in seiner Gesamtheit abrufen / festlegen müssen, um massive Dokumente weiterzugeben , die entweder die gesamte Site oder einen Beitrag mit viel darstellen Kommentare dazu können zu einem Problem werden, das Sie vermeiden möchten.

In dem Fall, in dem Posts getrennt von Kommentaren modelliert werden und zwei Personen einen Kommentar zu einer Story abgeben, werden diese einfach zu zwei "Kommentar" -Dokumenten in dieser Datenbank, ohne dass es zu Konflikten kommt. Nur zwei PUT-Operationen, um der "Kommentar" -Datenbank zwei neue Kommentare hinzuzufügen.

Um dann die Ansichten zu schreiben, die Ihnen die Kommentare für einen Beitrag zurückgeben, übergeben Sie die postID und geben dann alle Kommentare aus, die auf diese übergeordnete Beitrags-ID verweisen, sortiert in einer logischen Reihenfolge. Vielleicht übergeben Sie sogar etwas wie [postID, byUsername] als Schlüssel für die Ansicht "Kommentare", um den übergeordneten Beitrag anzugeben und anzugeben, wie die Ergebnisse sortiert werden sollen, oder etwas in dieser Richtung.

MongoDB behandelt Dokumente etwas anders, sodass Indizes auf Unterelementen eines Dokuments erstellt werden können. Daher wird möglicherweise dieselbe Frage in der MongoDB-Mailingliste angezeigt, und jemand sagt: "Machen Sie die Kommentare einfach zu einer Eigenschaft des übergeordneten Posts."

Aufgrund der Schreibsperre und des Single-Master-Charakters von Mongo würde das widersprüchliche Revisionsproblem von zwei Personen, die Kommentare hinzufügen, dort nicht auftauchen, und die Abfragefähigkeit des Inhalts wird, wie erwähnt, aufgrund von Sub- nicht zu schlecht beeinflusst Indizes.

Abgesehen davon, wenn Ihre Unterelemente in einer der DBs riesig sein werden (z. B. Zehntausende von Kommentaren), glaube ich, dass es die Empfehlung beider Lager ist, diese separaten Elemente zu erstellen. Ich habe sicherlich gesehen, dass dies bei Mongo der Fall ist, da es einige Obergrenzen für die Größe eines Dokuments und seiner Unterelemente gibt.

Riad Kalla
quelle
Sehr hilfreich. Vielen Dank
Ray Suelzer