Zasady modelowania dokumentów CouchDB


120

Mam pytanie, na które od jakiegoś czasu próbuję odpowiedzieć, ale nie mogę tego rozgryźć:

Jak projektujesz lub dzielisz dokumenty CouchDB?

Weźmy na przykład post na blogu.

Pół „relacyjny” sposób na zrobienie tego polegałby na stworzeniu kilku obiektów:

  • Poczta
  • Użytkownik
  • Komentarz
  • Etykietka
  • Skrawek

To ma sens. Ale próbuję użyć couchdb (z tych wszystkich powodów, że jest świetny) do modelowania tego samego i jest to niezwykle trudne.

Większość postów na blogach zawiera prosty przykład, jak to zrobić. Zasadniczo dzielą go w ten sam sposób, ale mówią, że można dodać „dowolne” właściwości do każdego dokumentu, co jest zdecydowanie miłe. Więc miałbyś coś takiego w CouchDB:

  • Opublikuj (z tagami i fragmentami „pseudo” modeli w dokumencie)
  • Komentarz
  • Użytkownik

Niektórzy ludzie powiedzieliby nawet, że możesz wrzucić tam komentarz i użytkownika, więc masz to:


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"
        }
    }
}

To wygląda bardzo ładnie i jest łatwe do zrozumienia. Rozumiem również, w jaki sposób można pisać widoki, które wyodrębniają tylko komentarze ze wszystkich dokumentów Post, aby umieścić je w modelach komentarzy, tak samo z użytkownikami i tagami.

Ale potem myślę: „dlaczego nie umieścić całej mojej witryny w jednym dokumencie?”:


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"
                    ...
                }
            }
        }
    }
}

Możesz łatwo tworzyć widoki, aby znaleźć to, czego szukasz.

W takim razie mam pytanie, jak określić, kiedy podzielić dokument na mniejsze, a kiedy dokonać „RELACJI” między dokumentami?

Myślę, że byłoby znacznie bardziej „zorientowane obiektowo” i łatwiej byłoby odwzorować je na Obiekty Wartości, gdyby zostały podzielone w ten sposób:


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
    }
}

... ale potem zaczyna wyglądać bardziej jak relacyjna baza danych. Często dziedziczę coś, co wygląda jak „cała witryna w dokumencie”, więc trudniej jest to modelować za pomocą relacji.

Przeczytałem wiele rzeczy o tym, jak / kiedy używać relacyjnych baz danych w porównaniu z bazami danych dokumentów, więc nie jest to główny problem. Bardziej zastanawiam się, jaką dobrą regułę / zasadę zastosować podczas modelowania danych w CouchDB.

Innym przykładem są pliki / dane XML. Niektóre dane XML są zagnieżdżone ponad 10 poziomów i chciałbym sobie wyobrazić, że używając tego samego klienta (na przykład Ajax on Rails lub Flex), wyrenderowałbym JSON z ActiveRecord, CouchRest lub dowolnego innego Object Relational Mapper. Czasami dostaję ogromne pliki XML, które stanowią całą strukturę witryny, jak ta poniżej, i musiałbym zmapować je na Obiekty Wartości, aby użyć ich w mojej aplikacji Rails, więc nie muszę pisać innego sposobu serializacji / deserializacji danych :


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

Więc ogólne pytania CouchDB to:

  1. Jakich reguł / zasad używasz, aby podzielić swoje dokumenty (relacje itp.)?
  2. Czy można umieścić całą witrynę w jednym dokumencie?
  3. Jeśli tak, w jaki sposób radzisz sobie z serializacją / deserializacją dokumentów z dowolnymi poziomami głębokości (jak powyższy przykład dużego json lub przykład XML)?
  4. A może nie przekształcasz ich w VO, czy po prostu zdecydujesz, że „te są zbyt zagnieżdżone w Object-Relational Map, więc po prostu uzyskam do nich dostęp za pomocą surowych metod XML / JSON”?

Wielkie dzięki za pomoc, kwestia podziału danych w CouchDB była dla mnie trudna do powiedzenia „tak mam to robić od teraz”. Mam nadzieję, że wkrótce się tam dostanę.

Przestudiowałem następujące strony / projekty.

  1. Hierarchiczne dane w CouchDB
  2. CouchDB Wiki
  3. Sofa - CouchDB App
  4. CouchDB The Definitive Guide
  5. Screencast PeepCode CouchDB
  6. CouchRest
  7. CouchDB README

... ale nadal nie odpowiedzieli na to pytanie.


2
wow napisałeś tutaj cały esej ... :-)
Eero

8
hej, to dobre pytanie
elmarco

Odpowiedzi:


26

Było już kilka świetnych odpowiedzi na to pytanie, ale chciałem dodać kilka nowszych funkcji CouchDB do zestawu opcji pracy z oryginalną sytuacją opisaną przez viatropos.

Kluczowym punktem dzielenia dokumentów jest miejsce, w którym mogą wystąpić konflikty (jak wspomniano wcześniej). Nigdy nie powinieneś przechowywać razem masowo „splątanych” dokumentów w jednym dokumencie, ponieważ otrzymasz pojedynczą ścieżkę zmiany dla całkowicie niezwiązanych ze sobą aktualizacji (na przykład dodanie komentarza dodającego poprawkę do całego dokumentu witryny). Zarządzanie relacjami lub połączeniami między różnymi, mniejszymi dokumentami może być początkowo mylące, ale CouchDB zapewnia kilka opcji łączenia różnych elementów w pojedyncze odpowiedzi.

Pierwszy duży to sortowanie widoków. Kiedy emitujesz pary klucz / wartość do wyników zapytania mapowania / redukcji, klucze są sortowane na podstawie sortowania UTF-8 („a” występuje przed „b”). Można też wyjść złożone klucze z mapy / zmniejszyć JSON jako tablice: ["a", "b", "c"]. Pozwoliłoby to na dołączenie pewnego rodzaju "drzewa" zbudowanego z kluczy tablicowych. Korzystając z powyższego przykładu, możemy wyprowadzić post_id, następnie typ rzeczy, do której się odwołujemy, a następnie jej identyfikator (w razie potrzeby). Jeśli następnie wyprowadzimy identyfikator dokumentu, do którego się odwołujemy, do obiektu w zwracanej wartości, możemy użyć parametru zapytania „include_docs”, aby uwzględnić te dokumenty w mapie / zmniejszyć dane wyjściowe:

{"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"}}
]}

Żądanie tego samego widoku z „? Include_docs = true” spowoduje dodanie klucza „doc”, który będzie używał „_id”, do którego odwołuje się obiekt „value”, lub jeśli nie ma go w obiekcie „value”, użyje „_id” dokumentu, z którego wyemitowano wiersz (w tym przypadku dokument „post”). Należy pamiętać, że wyniki te będą zawierać pole „id” odnoszące się do dokumentu źródłowego, z którego dokonano emisji. Zostawiłem to ze względu na miejsce i czytelność.

Następnie możemy użyć parametrów „start_key” i „end_key”, aby przefiltrować wyniki do danych pojedynczego posta:

? start_key = ["123412804910820"] & end_key = ["123412804910820", {}, {}]
Lub nawet wyodrębnij listę dla określonego typu:
? start_key = ["123412804910820", "komentarz"] & end_key = ["123412804910820", "komentarz", {}]
Te kombinacje parametrów zapytania są możliwe, ponieważ pusty obiekt („ {}”) jest zawsze na dole sortowania, a null lub „” są zawsze na górze.

Drugim pomocnym dodatkiem z CouchDB w takich sytuacjach jest funkcja _list. Umożliwiłoby to uruchomienie powyższych wyników za pomocą jakiegoś systemu szablonów (jeśli chcesz z powrotem HTML, XML, CSV lub cokolwiek innego) lub wygenerowanie ujednoliconej struktury JSON, jeśli chcesz mieć możliwość zażądania całej treści postu (w tym dane autora i komentarza) za pomocą jednego żądania i zwrócone jako pojedynczy dokument JSON, który jest zgodny z wymaganiami Twojego kodu interfejsu użytkownika / klienta. Pozwoliłoby to na zażądanie ujednoliconego dokumentu wyjściowego postu w następujący sposób:

/ db / _design / app / _list / posts / unified ?? start_key = ["123412804910820"] & end_key = ["123412804910820", {}, {}] & include_docs = true
Twoja funkcja _list (w tym przypadku o nazwie „zunifikowana”) pobierze wyniki mapy widoku / redukuj (w tym przypadku o nazwie „posty”) i uruchomi je za pomocą funkcji JavaScript, która odeśle odpowiedź HTTP w typie treści, potrzeba (JSON, HTML itp.).

Łącząc te elementy, możesz podzielić dokumenty na dowolnym poziomie, który uznasz za użyteczny i „bezpieczny” pod kątem aktualizacji, konfliktów i replikacji, a następnie złożyć je z powrotem w razie potrzeby, gdy zostaną o to poproszone.

Mam nadzieję, że to pomoże.


2
Nie jestem pewien, czy to pomogło Lance'owi, ale wiem jedno; zdecydowanie pomogło mi to bardzo! To jest niesamowite!
Mark

17

Wiem, że to stare pytanie, ale natknąłem się na nie, próbując znaleźć najlepsze podejście do tego samego problemu. Christopher Lenz napisał fajny wpis na blogu o metodach modelowania „złączeń” w CouchDB . Jedno z moich wniosków brzmiało: „Jedynym sposobem na umożliwienie dodawania powiązanych danych bez konfliktów jest umieszczenie tych danych w oddzielnych dokumentach”. Tak więc, dla uproszczenia, chciałbyś skłaniać się ku „denormalizacji”. Ale w pewnych okolicznościach napotkasz naturalną barierę z powodu sprzecznych zapisów.

W Twoim przykładzie Postów i komentarzy, jeśli pojedynczy post i wszystkie jego komentarze znajdowały się w jednym dokumencie, dwie osoby próbujące opublikować komentarz w tym samym czasie (tj. Przeciwko tej samej wersji dokumentu) spowodowałyby konflikt. Sytuacja pogorszyłaby się jeszcze w przypadku scenariusza „cała witryna w jednym dokumencie”.

Myślę więc, że ogólną zasadą byłoby „denormalizowanie, aż boli”, ale punkt, w którym będzie to „boleć”, jest wtedy, gdy istnieje duże prawdopodobieństwo opublikowania wielu zmian w tej samej wersji dokumentu.


Ciekawa odpowiedź. Mając to na uwadze, należy zapytać, czy jakakolwiek witryna o stosunkowo dużym ruchu miałaby wszystkie komentarze do pojedynczego posta na blogu w jednym dokumencie. Jeśli dobrze przeczytałem, oznacza to, że za każdym razem, gdy ludzie dodają komentarze w krótkich odstępach czasu, być może będziesz musiał rozwiązać konflikty. Oczywiście nie wiem, jak szybko musieliby to zrobić po kolei.
pc1oad1etter

1
W przypadku, gdy komentarze są częścią dokumentu w Couch, jednoczesne komentarze mogą powodować konflikt, ponieważ zakres wersjonowania to „post” ze wszystkimi jego komentarzami. W przypadku, gdy każdy z twoich obiektów jest zbiorem dokumentów, stałyby się one po prostu dwoma nowymi dokumentami z komentarzami z linkami do postu i bez obaw o kolizję. Chciałbym również zwrócić uwagę, że budowanie poglądów na "obiektowym" projektowaniu dokumentów jest proste - na przykład podajesz klucz posta, a następnie wysyłasz wszystkie komentarze, posortowane jakąś metodą, do tego postu.
Riyad Kalla

16

Książka mówi, jeśli dobrze pamiętam, do denormalize aż „boli”, zachowując częstotliwość, z jaką dokumenty mogą być aktualizowane w umyśle.

  1. Jakich reguł / zasad używasz, aby podzielić swoje dokumenty (relacje itp.)?

Z reguły podaję wszystkie dane, które są potrzebne do wyświetlenia strony dotyczącej danego przedmiotu. Innymi słowy, wszystko, co wydrukowałbyś na prawdziwej kartce papieru, którą byś komuś podał. Np. Dokument dotyczący notowań giełdowych oprócz numerów zawierałby nazwę firmy, giełdę, walutę; dokument umowy zawierałby nazwy i adresy kontrahentów, wszystkie informacje o datach i sygnatariuszach. Ale notowania giełdowe z różnych dat stanowiłyby oddzielne dokumenty, oddzielne umowy tworzyłyby oddzielne dokumenty.

  1. Czy można umieścić całą witrynę w jednym dokumencie?

Nie, to byłoby głupie, ponieważ:

  • musiałbyś czytać i pisać całą witrynę (dokument) przy każdej aktualizacji, a to jest bardzo nieefektywne;
  • nie skorzystasz z buforowania widoków.

3
Dziękuję, że trochę się ze mną zaangażowałeś. Przychodzi mi do głowy pomysł „uwzględnij wszystkie dane potrzebne do wyświetlenia strony dotyczącej danego elementu”, ale nadal jest to bardzo trudne do wdrożenia. „Strona” może być stroną z komentarzami, stroną użytkowników, stroną z wpisami lub stroną z komentarzami i postami, itp. Jak w takim razie podzielilibyście je? Możesz również wyświetlić swoją umowę z użytkownikami. Dostaję dokumenty „w formie formularza”, więc warto trzymać je oddzielnie.
Lance Pollard

6

Myślę, że odpowiedź Jake'a wskazuje na jeden z najważniejszych aspektów pracy z CouchDB, który może pomóc w podjęciu decyzji dotyczącej zakresu: konflikty.

W przypadku, gdy masz komentarze jako właściwość tablicową samego posta i masz tylko bazę danych 'post' z wieloma ogromnymi dokumentami 'post', jak słusznie zauważyli Jake i inni, możesz sobie wyobrazić scenariusz na bardzo popularny post na blogu, w którym dwóch użytkowników jednocześnie przesyła zmiany do dokumentu wiadomości, co powoduje kolizję i konflikt wersji dla tego dokumentu.

POZA STRONĄ: Jak wskazuje ten artykuł , weź pod uwagę również, że za każdym razem, gdy prosisz o ten dokument / aktualizujesz, musisz pobrać / ustawić dokument w całości, więc omijając ogromne dokumenty, które reprezentują całą witrynę lub post z dużą ilością komentarzy na ten temat może stać się problemem, którego chciałbyś uniknąć.

W przypadku, gdy posty są modelowane oddzielnie od komentarzy i dwie osoby przesyłają komentarz do historii, stają się one po prostu dwoma dokumentami „komentującymi” w tej bazie danych, bez problemu z konfliktem; tylko dwie operacje PUT, aby dodać dwa nowe komentarze do bazy danych „komentarz”.

Następnie, aby napisać widoki, które zwracają komentarze do posta, należy przekazać identyfikator posta, a następnie wyemitować wszystkie komentarze odwołujące się do tego identyfikatora wiadomości nadrzędnej, posortowane w logicznej kolejności. Może nawet przekazujesz coś takiego jak [postID, byUsername] jako klucz do widoku „komentarze”, aby wskazać post nadrzędny i sposób, w jaki chcesz posortować wyniki lub coś podobnego.

MongoDB obsługuje dokumenty w nieco inny sposób, umożliwiając budowanie indeksów na podelementach dokumentu, więc możesz zobaczyć to samo pytanie na liście mailingowej MongoDB i kogoś, kto mówi „po prostu uczyń komentarze właściwością postu nadrzędnego”.

Ze względu na blokowanie zapisu i charakter pojedynczego wzorca w Mongo, konfliktowy problem z wersją dwóch osób dodających komentarze nie pojawiłby się w tym miejscu, a możliwość wyszukiwania treści, jak wspomniano, nie ma zbyt słabego wpływu z powodu indeksy.

Biorąc to pod uwagę, jeśli twoje elementy podrzędne w którymkolwiek DB będą ogromne (powiedzmy 10 tysięcy komentarzy), uważam, że zaleceniem obu obozów jest zrobienie tych oddzielnych elementów; Z pewnością widziałem, że tak jest w przypadku Mongo, ponieważ istnieją pewne górne ograniczenia dotyczące wielkości dokumentu i jego podelementów.


Bardzo pomocne. Dziękuję
Ray Suelzer
Korzystając z naszej strony potwierdzasz, że przeczytałeś(-aś) i rozumiesz nasze zasady używania plików cookie i zasady ochrony prywatności.
Licensed under cc by-sa 3.0 with attribution required.