CouchDB文档建模原则

126
我有一个问题,我已经尝试回答了一段时间,但是无法解决:

你如何设计或划分CouchDB文档? 以博客文章为例。

半“关系型”的方法是创建几个对象:

  • 帖子
  • 用户
  • 评论
  • 标签
  • 片段

这非常有道理。 但是我正在尝试使用CouchDB(因为它的很多优点)来模拟相同的东西,结果非常困难。

大多数博客文章都会给你一个简单的示例。 它们基本上以相同的方式分割它,但是说您可以向每个文档添加“任意”属性,这绝对很好。 因此,在CouchDB中会有类似以下内容:

  • 帖子(包含文档中的标签和片段“伪”模型)
  • 评论
  • 用户

有些人甚至会说,您可以将评论和用户放在其中,这样就会得到以下内容:


<code>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"
        }
    }
}</code>
那看起来非常漂亮,而且易于理解。 我还了解到,您可以编写视图,从所有帖子文档中提取评论,以将它们转换为评论模型,与用户和标签一样。
但是我又想,"为什么不把我的整个网站放在一个文档中呢?"

<code>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"
                    ...
                }
            }
        }
    }
}</code>
你可以很容易地使用这个来创建视图以查找想要的内容。
那么我现在的问题是,如何确定将文档分成更小的文档,或者何时创建文档之间的“关系”?
如果按照这样的方式进行分割,它会更符合“面向对象”的设计,并且更容易映射为值对象:

<code>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
    }
}</code>
...但是它开始看起来更像关系型数据库。而且通常我会继承一些看起来像“整站在一个文档中”的东西,所以用关系来建模更加困难。

我已经阅读了很多关于何时使用关系数据库与文档数据库的文章,所以这不是主要问题。我更想知道,在建模CouchDB数据时应该应用什么样的良好规则/原则。

另一个例子是XML文件/数据。有些XML数据嵌套深度超过10级,我想使用相同的客户端(例如Ajax on Rails或Flex)来可视化它,就像渲染JSON从ActiveRecord、CouchRest或任何其他ORM一样。有时我会得到整个站点结构的大型XML文件,例如下面的文件,我需要将其映射到值对象中,以便在我的Rails应用程序中使用,这样我就不必编写另一种序列化/反序列化数据的方式:


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

所以一般的CouchDB问题是:

  1. 您使用什么规则/原则来划分文档(关系等)?
  2. 将整个站点放入一个文档中是否可以?
  3. 如果可以,您如何处理具有任意深度级别的序列化/反序列化文档(例如上面的大型JSON示例或XML示例)?
  4. 还是您不将它们转换为VO,只是决定“这些太嵌套了无法使用对象关系映射,因此我将使用原始的XML / JSON方法访问它们”?

非常感谢您的帮助,对于如何使用CouchDB划分数据的问题一直困扰着我,希望能尽快解决。

我研究了以下网站/项目。

  1. 在CouchDB中存储分层数据
  2. CouchDB维基
  3. Sofa - CouchDB应用程序
  4. CouchDB权威指南
  5. PeepCode CouchDB视频教程
  6. CouchRest
  7. CouchDB自述文件

...但它们仍未回答此问题。


2
哇,你在这里写了一篇完整的文章... :-) - Eero
9
嘿,这是个好问题。 - elmarco
4个回答

26

这个问题已经有了很好的答案,但我想添加一些最近的CouchDB功能来解决viatropos描述的原始情况。需要拆分文档的关键点是可能存在冲突的地方(如前所述)。您不应将高度“交织”的文档合并为单个文档,因为您将获得完全无关的更新(例如添加评论将为整个站点文档添加一个版本路径)。管理各种较小文档之间的关系或连接可能会令人困惑,但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 在这些情况下的第二个有用的附加功能是 _list 函数。它允许您通过某种模板系统(如果需要 HTML、XML、CSV 或其他内容),或者在您想要能够请求整个帖子的内容(包括作者和评论数据)并返回与客户端/UI 代码所需相匹配的单个 JSON 文档的情况下,输出一个统一的 JSON 结构。这样可以通过以下方式请求帖子的统一输出文档:
/db/_design/app/_list/posts/unified??start_key=["123412804910820"]&end_key=["123412804910820", {}, {}]&include_docs=true
您的 _list 函数(在本例中命名为 "unified")将接受视图映射/归约(在本例中命名为 "posts")的结果,并将它们通过 JavaScript 函数运行,以便以您需要的内容类型(JSON、HTML 等)发送 HTTP 响应。
通过组合这些功能,您可以将文档分割成您发现有用和“安全”的更新、冲突和复制级别,然后根据需要将它们重新组合。希望这能帮到您。

2
不确定这是否对Lance有帮助,但我知道一件事:它绝对对我有很大帮助!这太棒了! - Mark

18
我知道这是一个老问题,但我遇到了它,试图找出解决这个问题的最佳方法。Christopher Lenz在CouchDB中建模“连接”的方法方面写了一篇不错的博客文章。我的收获之一是:“允许非冲突添加相关数据的唯一方法是将相关数据放入单独的文档中。”因此,为了简单起见,您应该倾向于“去规范化”。但在某些情况下,由于冲突写入,您将遇到自然障碍。
例如,在您的帖子和评论示例中,如果单个帖子及其所有评论都存在一个文档中,则两个人同时尝试发布评论(即针对文档的相同修订版本)将导致冲突。在您的“整个站点在单个文档中”的情况下,情况会变得更糟。
因此,我认为经验法则是“去规范化直到受到限制”,但当您有很高的多次编辑可能发布在文档的相同修订版本时,就会达到“受到限制”的程度。

有趣的回复。考虑到这一点,人们应该质疑任何相当高的流量网站是否会将单个博客文章的所有评论都放在一个文档中。如果我理解正确,这意味着每当有人快速添加评论时,您可能必须解决冲突。当然,我不知道他们需要多快才能触发此操作。 - pc1oad1etter
2
在Couch中,如果评论是文档的一部分,同时发表评论可能会发生冲突,因为您的版本控制范围是“帖子”及其所有评论。如果您的每个对象都是文档集合,则这些对象将变成两个新的“评论”文档,并链接回帖子,不必担心冲突。我还要指出,在“面向对象”的文档设计上构建视图很简单--例如,您传递一个帖子的键,然后发出该帖子的所有评论,按某种方法排序。 - Riyad Kalla

16

这本说,如果我没记错的话,要去正规化直到“它疼”,但要记住您的文档可能更新的频率。

  1. 您用什么规则/原则来划分文档(关系等)?

通常情况下,我会包含所有显示与问题相关的页面所需的数据。换句话说,你会在现实世界中交给别人的纸张上打印出来的所有内容。例如,股票报价文档将包括公司名称、交易所、货币以及数字;合同文档将包括交易对手的名称和地址、有关日期和签署者的所有信息。但是不同日期的股票报价将形成单独的文档,不同的合同将形成单独的文档。

  1. 把整个站点放入一个文档中可以吗?

不可以,因为:

  • 每次更新都必须读取和写入整个站点(文档),这非常低效;
  • 您将无法受益于任何视图缓存。

3
谢谢你和我交流。我明白“包括所有需要显示有关该项的页面所需数据”的概念,但实现起来仍然非常困难。一个“页面”可能是评论页、用户页、帖子页或评论和帖子页等等。那么你会如何分配它们?你也可以将你的合同与用户一起显示。我理解“类似表格”的文件,将它们保持分开是有意义的。 - Lance

6
我认为Jake的回答准确地指出了使用CouchDB时可能帮助您做出范围决策的最重要方面之一:冲突。
如果您将评论作为帖子本身的数组属性,并且只有一个“post”数据库,其中包含大量的“post”文档,则在真正受欢迎的博客文章上,两个用户同时提交对该文档的编辑,会导致冲突和版本冲突。
顺便说一下:正如这篇文章所指出的那样,每次请求/更新文档时,您必须完整获取/设置文档,因此传递代表整个站点或具有许多评论的帖子的大型文档可能会成为您想要避免的问题。 如果将帖子与评论分别建模,并且两个人在故事中发表评论,则这些评论只是该数据库中的两个“comment”文档,没有冲突问题;只需进行两个PUT操作以向“comment”数据库添加两个新评论即可。
然后编写视图,以返回帖子的评论,您将传递postID,然后发出所有引用该父帖子ID的评论,按某种逻辑排序。也许您甚至会将[postID,byUsername]之类的东西作为“comments”视图的键传递,以指示父帖子及其排序方式。
MongoDB处理文档的方式有些不同,允许在文档的子元素上构建索引,因此您可能会在MongoDB邮件列表上看到相同的问题,并且有人说“只需将评论作为父帖子的属性”。
由于Mongo的写入锁定和单主性质,两个人添加评论的冲突修订问题不会在那里出现,并且正如上述所述,内容的可查询性也不会受到子索引的影响。
话虽如此,如果您在任何一个数据库中的子元素都非常庞大(例如数万条评论),我认为两者都建议将其分开;我确实看到过这种情况在Mongo中发生,因为文档及其子元素的大小存在一些上限限制。

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接