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

看起来非常好,而且很容易理解。我也理解如何编写视图,该视图仅从所有Post文档中提取注释,以将它们放入Comment模型中,与Users and Tags相同。

但是后来我想:“为什么不将我的整个网站都放在一个文档中呢?”:


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或任何其他Object Relational Mapper渲染JSON相同的客户端(例如,Ajax on Rails或Flex)来可视化它。有时,我会得到庞大的XML文件,这些文件是整个网站结构的一部分,如下图所示,我需要将其映射到Value Objects以在Rails应用程序中使用,因此我不必编写另一种序列化/反序列化数据的方法:


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

因此,一般的CouchDB问题是:

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

非常感谢您的帮助,关于如何使用CouchDB划分数据的问题让我很难说“这是我从现在开始应该做的事情”。我希望能尽快到达那里。

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

  1. CouchDB中的分层数据
  2. CouchDB Wiki
  3. 沙发-CouchDB应用
  4. CouchDB权威指南
  5. PeepCode CouchDB屏幕录像
  6. CouchRest
  7. CouchDB自述文件

...但是他们仍然没有回答这个问题。


2
哇,您在这里写了整篇文章... :-)
Eero

8
嘿,这是一个很好的问题
elmarco

Answers:


26

已经有一些很好的答案,但是我想在一些选项中添加一些最新的CouchDB功能,以处理viatropos所描述的原始情况。

拆分文档的关键点是可能存在冲突的地方(如前所述)。永远不要将大量“纠结”的文档放在一个文档中,因为您将获得用于完全不相关的更新的单个修订路径(例如,添加注释以将修订添加到整个站点文档)。起初,管理各种较小的文档之间的关系或连接可能会造成混乱,但是CouchDB提供了几种将不同的片段组合成单个响应的选项。

第一个大的是视图整理。当您将键/值对发送到映射/归约查询的结果中时,键将基于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”,“评论”]&end_key = [“ 123412804910820”,“评论”,{}]
这些查询参数组合是可能的,因为空对象(“ {}”)始终位于排序规则的底部,而null或“”始终位于排序规则的底部。

在这种情况下,来自CouchDB的第二个有用的补充是_list函数。这将允许您通过某种模板系统(如果需要HTML,XML,CSV或其他格式)运行以上结果,或者如果希望能够请求整个帖子的内容(包括以下内容),则输出统一的JSON结构作者和评论数据),并以单个JSON文档形式返回,该文档与您的客户端/ UI代码需求相匹配。这样做将允许您以这种方式请求帖子的统一输出文档:

/ 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
不确定这是否对Lance有所帮助,但我知道一件事。它肯定对我有很大帮助!这太棒了!
2012年

17

我知道这是一个古老的问题,但是我遇到了这个问题,试图找出解决这个完全相同问题的最佳方法。Christopher Lenz 在CouchDB中写了一篇不错的博客文章,介绍了建模“ joins”的方法。我的收获之一是:“唯一允许无冲突地添加相关数据的方法是将相关数据放入单独的文档中。” 因此,为简单起见,您将倾向于“非规范化”。但是在某些情况下,由于写入冲突,您将遇到自然障碍。

在您的“帖子和评论”示例中,如果一个帖子及其所有评论都存在于一个文档中,那么两个人试图同时发表评论(即针对文档的同一修订版)将导致冲突。在“单个文档中的整个站点”场景中,情况甚至更糟。

因此,我认为经验法则将是“规范化,直到受到伤害”,但要“破坏”这一点是,您很有可能针对同一文档修订版发布多个编辑。


有趣的回应。考虑到这一点,应该问一个问题:在任何一个流量较高的网站上,是否甚至会在一个文档中包含单个博客文章的所有评论。如果我没看错,这意味着每次有人快速添加评论时,您可能都必须解决冲突。当然,我不知道他们必须如此迅速地触发这一点。
pc1oad1etter

1
如果注释是Couch中文档的一部分,则同时进行的注释发布可能会发生冲突,因为您的版本范围是包含所有注释的“发布”。在您的每个对象都是文档集合的情况下,这些对象将简单地成为两个新的“注释”文档,并带有指向该帖子的链接,而无需担心冲突。我还要指出,直接在“面向对象”文档设计上构建视图-例如,传递帖子的键,然后发出该帖子的所有注释(按某种方法排序)。
里亚德·卡拉

16

书中说,如果我没有记错,非规范化,直到“好痛”,同时考虑与哪些文件可能被更新的频率。

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

根据经验,我将包含显示有关该项目的页面所需的所有数据。换句话说,您将在现实世界的纸上打印的所有东西都将交给他人。例如,股票报价文件除数字外还应包括公司名称,交易所,货币。合同文件应包括交易对手的名称和地址,有关日期和签署人的所有信息。但是不同日期的股票报价将形成单独的文件,单独的合同将形成单独的文件。

  1. 可以将整个站点合并为一个文档吗?

不,那将是愚蠢的,因为:

  • 您将不得不在每次更新时读写整个站点(文档),这效率很低;
  • 您将不会从任何视图缓存中受益。

3
感谢您与我的交流。我的想法是“包括显示有关该项目的页面所需的所有数据”,但这仍然很难实现。“页面”可以是“评论”页面,“用户”页面,“帖子”页面或“评论和帖子”页面等。那么,您主要如何将它们划分?您也可以与用户一起显示您的合同。我得到了“类似表单”的文档,这使它们分开很有意义。
兰斯·波拉德

6

我认为Jake的回应是与CouchDB合作最重要的方面之一,它可以帮助您做出范围界定的决定:冲突。

在您将评论作为帖子本身的数组属性的情况下,您只是拥有一个带有大量巨大的“帖子”文档的“帖子”数据库,正如杰克和其他人正确指出的那样,您可以想象一个场景真正流行的博客文章,其中两个用户同时对文章文档进行编辑,从而导致该文档发生冲突和版本冲突。

旁白:正如本文所指出的,还要考虑到每次请求/更新该文档时,您都必须完整地获取/设置文档,因此要传递大量代表整个站点或大量内容的文档评论可能会成为您要避免的问题。

如果帖子是与评论分开建模的,并且两个人对一个故事发表评论,则这些帖子将成为该数据库中的两个“评论”文档,而不会产生冲突。只需执行两个PUT操作即可向“ comment”数据库添加两个新注释。

然后编写要返回帖子评论的视图,您将传入postID,然后发出所有引用该父帖子ID的评论(以某种逻辑顺序排序)。甚至您甚至可以将[postID,byUsername]之类的内容传递给“评论”视图,以指示父帖子以及您希望结果如何排序或类似的内容。

MongoDB处理文档的方式略有不同,允许在文档的子元素上建立索引,因此您可能会在MongoDB邮件列表上看到相同的问题,并且有人说“只需将评论作为父帖子的属性”。

由于Mongo具有写锁定和单主机特性,两个人添加注释的冲突版本问题就不会在那里出现了,并且如上所述,内容的可查询性不会因子目录而太差索引。

话虽如此,如果您在任一数据库中的子元素都将庞大(例如成千上万的评论),我相信这两个阵营都建议将这些元素分开;我当然已经看到了Mongo的情况,因为文档及其子元素的大小有一些上限。


很有帮助。谢谢
Ray Suelzer 2013年
By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.