CouchDBドキュメントのモデリングの原則


120

私は今しばらく答えようとしてきましたが、理解できない質問があります:

CouchDBのドキュメントをどのように設計または分割しますか?

たとえば、ブログ投稿を見てください。

これを行う「リレーショナル」な方法は、いくつかのオブジェクトを作成することです。

  • 役職
  • ユーザー
  • コメント
  • 鬼ごっこ
  • スニペット

これは非常に理にかなっています。しかし、私は同じことをモデル化するためにcouchdbを使用することを試みています(それがすべての理由で素晴らしい)ため、それは非常に困難でした。

そこにあるブログの投稿のほとんどは、これを行う方法の簡単な例を示しています。基本的には同じ方法で分割しますが、各ドキュメントに「任意」のプロパティを追加できると言っていて、これは間違いなく便利です。したがって、CouchDBには次のようなものがあります。

  • 投稿(ドキュメント内のタグとスニペット「疑似」モデルを使用)
  • コメント
  • ユーザー

コメントとユーザーをそこに投入できると言う人もいるでしょうから、次のようにします。


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

それはとても見栄えがよく、理解しやすいです。また、すべての投稿ドキュメントからコメントのみを抽出したビューを作成して、それらをユーザーやタグと同じようにコメントモデルに取り込む方法も理解しています。

しかし、「サイト全体を1つのドキュメントにまとめないのはなぜですか」と思います。


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

簡単にビューを作成して、必要なものを見つけることができます。

次に、私が持っている質問は、ドキュメントを小さいドキュメントに分割するタイミング、またはドキュメント間の「関係」を作成するタイミングをどのように決定するかです。

次のように分割すると、「オブジェクト指向」のほうがはるかに多くなり、値オブジェクトにマッピングしやすくなります。


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

...しかし、その後はリレーショナルデータベースのように見え始めます。また、「ドキュメント内のサイト全体」のようなものを継承することがよくあるため、リレーションでモデル化するのはより困難です。

私は、リレーショナルデータベースとドキュメントデータベースをどのように/いつ使用するかについて多くのことを読んだので、ここでは主な問題ではありません。CouchDBでデータをモデル化する際に適用すべき良いルール/原則は何でしょうか。

別の例は、XMLファイル/データです。一部のXMLデータには10レベル以上のネストがあり、ActiveRecord、CouchRest、またはその他のオブジェクトリレーショナルマッパーからJSONをレンダリングするのと同じクライアント(たとえば、Ajax on Rails、またはFlex)を使用してそれを視覚化したいと思います。以下のようなサイト構造全体である巨大なXMLファイルを取得することがあり、Railsアプリで使用するためにそれを値オブジェクトにマップする必要があるため、データをシリアル化/非シリアル化する別の方法を記述する必要がありません:


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

したがって、CouchDBに関する一般的な質問は次のとおりです。

  1. 文書を分割するためにどのようなルール/原則を使用していますか(関係など)?
  2. サイト全体を1つのドキュメントにまとめても問題ありませんか?
  3. もしそうなら、(上記の大きなjsonの例やxmlの例のように)任意の深度レベルを持つドキュメントのシリアライズ/デシリアライズをどのように処理しますか?
  4. または、それらをVOに変換せずに、「これらはオブジェクトリレーショナルマップにネストされすぎているため、生のXML / JSONメソッドを使用してアクセスするだけ」と決めましたか?

どうもありがとうございました。CouchDBを使用してデータを分割する方法の問題は、「これが私がこれからどうやってやるべきか」と言うのが困難でした。早く着くといいな。

以下のサイト/プロジェクトを調査しました。

  1. CouchDBの階層データ
  2. CouchDB Wiki
  3. ソファ-CouchDBアプリ
  4. CouchDB決定版ガイド
  5. PeepCode CouchDBスクリーンキャスト
  6. カウチレスト
  7. CouchDB README

...しかし、彼らはまだこの質問に答えていません。


2
うわー、ここでエッセイを全部書いています... :-)
Eero

8
ねえ、それは良い質問です
elmarco

回答:


26

これには素晴らしい答えがすでにいくつかありますが、viatroposで説明されている元の状況を処理するためのオプションの組み合わせに、より最近のCouchDB機能をいくつか追加したいと思いました。

ドキュメントを分割する重要なポイントは、競合が発生する可能性がある場所です(前述のとおり)。完全に無関係な更新(たとえば、サイトのドキュメント全体にコメントを追加するコメント)の単一のリビジョンパスを取得するため、大量の「絡み合った」ドキュメントを1つのドキュメントにまとめないでください。さまざまな小さなドキュメント間の関係や接続を管理することは、最初は混乱する可能性がありますが、CouchDBには、異なる部分を単一の応答に結合するためのいくつかのオプションがあります。

最初の大きなものはビューの照合です。キーと値のペアをmap / reduceクエリの結果に出力すると、キーはUTF-8照合に基づいてソートされます( "a"は "b"の前)。map / reduceからJSON配列として複雑なキーを出力することもできます["a", "b", "c"]。そうすることで、配列キーから構築されたソートの「ツリー」を含めることができます。上記の例を使用して、post_id、次に参照しているもののタイプ、次にそのID(必要な場合)を出力できます。次に、参照されたドキュメントのIDを、返された値のオブジェクトに出力する場合、 'include_docs'クエリパラメータを使用して、それらのドキュメントをmap / reduce出力に含めることができます。

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

同じビューを '?include_docs = true'で要求すると、 'doc'キーが追加され、 'value'オブジェクトで参照される '_id'を使用するか、 'value'オブジェクトに存在しない場合は、行が発行されたドキュメントの '_id'(この場合は 'post'ドキュメント)。これらの結果には、発行が行われたソースドキュメントを参照する「id」フィールドが含まれることに注意してください。スペースと読みやすさのために省略しました。

次に、「start_key」パラメータと「end_key」パラメータを使用して、結果を単一の投稿のデータに絞り込むことができます。

?start_key = ["123412804910820"]&end_key = ["123412804910820"、{}、{}]
または、特定のタイプのリストを具体的に抽出します。
?start_key = ["123412804910820"、 "comment"]&end_key = ["123412804910820"、 "comment"、{}]
空のオブジェクト( " {}")は常に照合の一番下にあり、nullまたは ""は常に一番上にあるため、これらのクエリとパラメーターの組み合わせは可能です。

これらの状況でCouchDBから2番目に役立つ追加機能は、_list関数です。これにより、上記の結果を何らかのテンプレートシステム(HTML、XML、CSVなどに戻したい場合)を介して実行したり、投稿のコンテンツ全体(リクエストを含む)をリクエストしたい場合に統合JSON構造を出力したりできます。作成者とコメントデータ)を1つのリクエストで送信し、クライアント側/ UIコードが必要とするものと一致する1つのJSONドキュメントとして返されます。これを行うと、次のように投稿の統合出力ドキュメントをリクエストできます。

/ db / _design / app / _list / posts / unified ?? start_key = ["123412804910820"]&end_key = ["123412804910820"、{}、{}]&include_docs = true
_list関数(この場合は「unified」という名前)は、ビューmap / reduce(この場合は「posts」という名前)の結果を受け取り、JavaScriptタイプの関数を実行して、コンテンツタイプのHTTP応答を送り返します。 (JSON、HTMLなど)が必要です。

これらを組み合わせると、更新、競合、および複製に対して有用で「安全」と思われるレベルでドキュメントを分割し、要求されたときに必要に応じて元に戻すことができます。

お役に立てば幸いです。


2
これがランスに役立ったかどうかはわかりませんが、私は1つ知っています。それは間違いなく私に大きな助けとなりました!これはすごい!
マーク

17

私はこれが古い質問であることを知っていますが、このまったく同じ問題への最善のアプローチを見つけようとしてそれを見つけました。Christopher LenzがCouchDBで「結合」をモデル化する方法についての素晴らしいブログ投稿を書きました。私が持ち帰ったことの1つは、「関連するデータを競合することなく追加できるようにする唯一の方法は、その関連するデータを個別のドキュメントに入れることです」でした。したがって、簡単にするために、「非正規化」に傾くことをお勧めします。ただし、特定の状況では書き込みが競合するため、自然な障壁にぶつかります。

投稿とコメントの例では、単一の投稿とそのすべてのコメントが1つのドキュメントにある場合、2人が同時にコメントを投稿しようとすると(つまり、ドキュメントの同じリビジョンに対して)、競合が発生します。これは、「単一ドキュメント内のサイト全体」のシナリオではさらに悪化します。

したがって、経験則は「痛むまで非正規化」になると思いますが、「痛む」ポイントは、ドキュメントの同じリビジョンに対して複数の編集が投稿される可能性が高いところです。


興味深い反応。そのことを念頭に置いて、1つのドキュメントに1つのブログ投稿に対するすべてのコメントが適度に高トラフィックのサイトにあるかどうかを質問する必要があります。私がこれを正しく読んだ場合、それはあなたが人々がすぐに連続してコメントを追加するたびに、あなたは衝突を解決しなければならないかもしれないことを意味します。もちろん、私は彼らがこれをトリガーするためにどれほど速く続く必要があるかを知りません。
pc1oad1etter

1
コメントがCouchのドキュメントの一部である場合、バージョンのスコープはすべてのコメントを含む「投稿」であるため、同時コメント投稿は競合する可能性があります。各オブジェクトがドキュメントのコレクションである場合、これらは単にポストへのリンクがあり、衝突の心配がない2つの新しい「コメント」ドキュメントになります。また、「オブジェクト指向」ドキュメントデザインのビューを構築するのは簡単です。たとえば、投稿のキーを渡して、その投稿に対して、何らかの方法でソートされたすべてのコメントを出力します。
リヤドカラ2011

16

このは、私が正しく思い出せば、ドキュメントが更新される頻度を考慮しながら、「痛い」になるまで非正規化することを述べています。

  1. 文書を分割するためにどのようなルール/原則を使用していますか(関係など)?

経験則として、問題のアイテムに関するページを表示するために必要なすべてのデータを含めます。つまり、実際に紙に印刷して誰かに渡すすべてのものです。たとえば、株価情報ドキュメントには、数字に加えて、会社名、取引所、通貨が含まれます。契約書には、取引相手の名前と住所、日付と署名者に関するすべての情報が含まれます。しかし、異なる日付の株価情報は個別のドキュメントを形成し、個別の契約は個別のドキュメントを形成します。

  1. サイト全体を1つのドキュメントにまとめても問題ありませんか?

いいえ、それはばかげています。

  • 更新のたびにサイト全体(ドキュメント)を読み書きする必要があり、それは非常に非効率的です。
  • ビューのキャッシュによるメリットはありません。

3
私と一緒に少し入ってくれてありがとう。「問題のアイテムに関するページを表示するために必要なすべてのデータを含める」という考え方はわかりますが、それでも実装は非常に困難です。「ページ」には、コメントのページ、ユーザーのページ、投稿のページ、コメントと投稿のページなどがあります。その場合、主にどのように分割しますか?契約をユーザーとともに表示することもできます。私は「フォームのような」ドキュメントを取得します。それらを分離しておくのは理にかなっています。
ランスポラール

6

ジェイクの対応は、スコーピングの決定に役立つ可能性のあるCouchDBとの連携の最も重要な側面の1つである競合を否定していると思います。

投稿自体の配列プロパティとしてコメントがあり、Jakeや他の人が正しくシナリオを想像できると指摘したように、大量の「投稿」ドキュメントがたくさんある「投稿」DBがある場合2人のユーザーが同時に編集を投稿ドキュメントに送信する非常に人気のあるブログ投稿で、そのドキュメントの衝突とバージョン競合が発生します。

余談:として、この記事のポイントoutは、また、あなたが要求されるたびに/あなたが取得する必要があり、そのドキュメントを更新/ので、たくさんのサイト全体またはポストを表すいずれかの大規模な文書を周りに渡し、その全体が文書を設定することを検討してくださいそれについてのコメントのうち、あなたが避けたい問題になる可能性があります。

投稿がコメントとは別にモデル化され、2人がストーリーについてコメントを送信する場合、それらは単にそのDB内の2つの「コメント」ドキュメントになり、競合の問題はありません。「コメント」データベースに2つの新しいコメントを追加するための2つのPUT操作。

次に、投稿のコメントを返すビューを作成するには、postIDを渡し、その論理的な順序でソートされた、その親投稿IDを参照するすべてのコメントを発行します。おそらく、[postID、byUsername]のようなものを「コメント」ビューのキーとして渡して、親投稿と、結果を並べ替える方法、またはそれらの行に沿って何かを示すこともできます。

MongoDBはドキュメントを少し異なる方法で処理し、ドキュメントのサブ要素にインデックスを作成できるようにします。そのため、MongoDBメーリングリストで同じ質問が表示され、「コメントを親投稿のプロパティにする」と誰かが言うかもしれません。

Mongoの書き込みロックとシングルマスターの性質のため、コメントを追加する2人のユーザーの競合するリビジョンの問題が発生することはなく、コンテンツのクエリ機能は、前述のように、サブインデックス。

そうは言っても、いずれかの DBのサブ要素が巨大になる場合(たとえば、数万のコメント)は、これらの別々の要素を作成することが両方の陣営の推奨であると思います。ドキュメントとそのサブエレメントのサイズに上限があるため、Mongoの場合もそうだと私は確信しています。


非常に役立ちます。ありがとう
Ray Suelzer、2013年
弊社のサイトを使用することにより、あなたは弊社のクッキーポリシーおよびプライバシーポリシーを読み、理解したものとみなされます。
Licensed under cc by-sa 3.0 with attribution required.