CouchDB 文档建模原则
Posted
技术标签:
【中文标题】CouchDB 文档建模原则【英文标题】:Principles for Modeling CouchDB Documents 【发布时间】:2010-12-04 14:00:07 【问题描述】:我有一个问题我一直在尝试回答一段时间但无法弄清楚:
您如何设计或分割 CouchDB 文档?
以博客文章为例。
半“关系”的方式是创建一些对象:
发布 用户 评论 标签 片段这很有意义。但我正在尝试使用 couchdb(因为它很棒的所有原因)来建模同样的东西,而且非常困难。
那里的大多数博客文章都为您提供了如何执行此操作的简单示例。他们基本上以相同的方式划分它,但是说您可以为每个文档添加“任意”属性,这绝对是不错的。所以你会在 CouchDB 中有这样的东西:
发布(在文档中带有标签和 sn-ps“伪”模型) 评论 用户有些人甚至会说你可以把评论和用户放在那里,所以你会有这个:
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 文档中提取评论的视图,以将它们放入评论模型中,与用户和标签相同。
但后来我想,“为什么不把我的整个网站放在一个文档中呢?”:
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 层,我想使用相同的客户端(例如 Ajax on Rails 或 Flex)来可视化它,我将从 ActiveRecord、CouchRest 或任何其他对象关系映射器呈现 JSON。有时我会得到巨大的 XML 文件,它们是整个站点结构,如下所示,我需要将其映射到值对象以在我的 Rails 应用程序中使用,这样我就不必编写另一种序列化/反序列化数据的方式:
<pages>
<page>
<subPages>
<subPage>
<images>
<image>
<url/>
</image>
</images>
</subPage>
</subPages>
</page>
</pages>
所以一般的 CouchDB 问题是:
-
您使用哪些规则/原则来划分文档(关系等)?
可以将整个网站放在一个文档中吗?
如果是这样,您如何处理具有任意深度级别的序列化/反序列化文档(如上面的大型 json 示例或 xml 示例)?
或者你不把它们变成 VO,你只是决定“这些太嵌套到对象关系映射,所以我将只使用原始 XML/JSON 方法访问它们”?
非常感谢您的帮助,如何用 CouchDB 划分数据的问题让我很难说“从现在开始我应该这样做”。我希望尽快到达那里。
我研究了以下网站/项目。
-
Hierarchical Data in CouchDB
CouchDB Wiki
Sofa - CouchDB App
CouchDB The Definitive Guide
PeepCode CouchDB Screencast
CouchRest
CouchDB README
...但他们仍然没有回答这个问题。
【问题讨论】:
哇,你在这里写了一篇完整的文章...... :-) 嘿,这是个好问题 【参考方案1】:book 说,如果我没记错的话,去规范化直到“痛”,同时记住你的文档可能更新的频率。
您使用哪些规则/原则来划分文档(关系等)?
根据经验,我包含了显示与相关项目相关的页面所需的所有数据。换句话说,您将在现实世界的一张纸上打印并交给某人的所有内容。例如。除数字外,股票报价文件还包括公司名称、交易所、货币;合同文件将包括交易对手的名称和地址、日期和签署人的所有信息。但是不同日期的股票报价将形成单独的文件,单独的合同将形成单独的文件。
可以将整个站点放在一个文档中吗?
不,那太傻了,因为:
您必须在每次更新时读取和写入整个网站(文档),效率非常低; 您不会从任何视图缓存中受益。【讨论】:
感谢您与我一起了解它。我的想法是“包括显示有关相关项目的页面所需的所有数据”,但这仍然很难实现。 “页面”可以是评论页面、用户页面、帖子页面或评论和帖子页面等。那么您主要如何划分它们?您还可以与用户一起显示您的合同。我得到了“类似表单”的文档,将它们分开是有意义的。【参考方案2】:我知道这是一个老问题,但我遇到了它,试图找出解决这个完全相同的问题的最佳方法。 Christopher Lenz 写了一篇关于methods of modeling "joins" in CouchDB 的精彩博文。我的收获之一是:“允许无冲突地添加相关数据的唯一方法是将相关数据放入单独的文档中。”因此,为简单起见,您希望倾向于“非规范化”。但是在某些情况下,由于写入冲突,您会遇到天然障碍。
在您的帖子和评论示例中,如果单个帖子及其所有 cmets 都存在于一个文档中,那么两个人试图同时发表评论(即针对文档的同一修订版)将导致冲突。在您的“单个文档中的整个站点”场景中,这会变得更糟。
所以我认为经验法则是“非规范化,直到它受伤”,但它会“受伤”的地方是您很有可能针对文档的同一修订发布多次编辑。
【讨论】:
有趣的回应。考虑到这一点,人们应该质疑任何合理的高流量网站是否会将单个博客文章的所有 cmets 放在一个文档中。如果我没看错,这意味着每次有人快速连续添加 cmets 时,您可能必须解决冲突。当然,我不知道他们必须连续多快才能触发这个。 如果 cmets 是 Couch 中文档的一部分,同时发表的评论可能会发生冲突,因为您的版本控制范围是包含所有 cmets 的“帖子”。在您的每个对象都是文档集合的情况下,这些将简单地成为两个新的“评论”文档,其中包含返回帖子的链接,并且无需担心冲突。我还要指出的是,在“面向对象”文档设计上构建视图是直截了当的——例如,您传入帖子的键,然后为该帖子发出所有 cmets,按某种方法排序。【参考方案3】:我认为 Jake 的回答指出了与 CouchDB 合作的最重要方面之一,这可能有助于您做出范围界定决定:冲突。
如果您将 cmets 作为帖子本身的数组属性,并且您只有一个“帖子”数据库,其中包含一堆巨大的“帖子”文档,正如 Jake 和其他人正确指出的那样,您可以想象一个非常受欢迎的博客文章中的场景,其中两个用户同时提交对文章文档的编辑,导致该文档发生冲突和版本冲突。
旁白:作为this article points out,还要考虑到每次您请求/更新该文档时,您都必须完整地获取/设置文档,因此传递代表整个站点或上面有很多 cmets 的帖子可能会成为您想要避免的问题。
在帖子与 cmets 分开建模并且两个人对故事提交评论的情况下,这些只是成为该数据库中的两个“评论”文档,没有冲突问题;只需两个 PUT 操作即可将两个新 cmets 添加到“评论”数据库。
然后,要编写返回帖子 cmets 的视图,您将传入 postID,然后发出引用该父帖子 ID 的所有 cmets,并按某种逻辑顺序排序。也许您甚至可以传入 [postID,byUsername] 之类的内容作为“cmets”视图的键,以指示父帖子以及您希望结果如何排序或类似的内容。
MongoDB 处理文档的方式略有不同,允许在文档的子元素上构建索引,因此您可能会在 MongoDB 邮件列表中看到相同的问题,并且有人说“只需将 cmets 设为父帖子的属性” .
由于 Mongo 的写入锁定和单主控特性,不会出现两个人添加 cmets 的冲突修订问题,并且如前所述,内容的查询能力不会影响太差,因为子索引。
话虽如此,如果您在任何一个 DB 中的子元素会很大(比如成千上万个 cmets),我相信两个阵营都建议制作这些单独的元素;我当然已经看到 Mongo 就是这种情况,因为文档及其子元素的大小有一些上限。
【讨论】:
非常有帮助。谢谢【参考方案4】:已经有一些很好的答案,但我想添加一些最新的 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 或其他任何东西)运行上述结果,或者如果您希望能够请求整个帖子的内容(包括作者和评论数据)通过单个请求返回,并作为单个 JSON 文档返回,该文档与您的客户端/UI 代码所需的内容相匹配。这样做可以让您以这种方式请求帖子的统一输出文档:
/db/_design/app/_list/posts/unified??start_key=["123412804910820"]&end_key=["123412804910820", , ]&include_docs=true您的 _list 函数(在此名为“unified”的案例)将获取视图 map/reduce 的结果(在本例中名为“posts”)并通过 javascript 函数运行它们,该函数将以您需要的内容类型(JSON、HTML、等等)。
结合这些内容,您可以将文档拆分到您认为对更新、冲突和复制有用且“安全”的任何级别,然后在需要时将它们重新组合在一起。
希望对您有所帮助。
【讨论】:
不确定这是否对 Lance 有帮助,但我知道一件事;这对我帮助很大!这太棒了!以上是关于CouchDB 文档建模原则的主要内容,如果未能解决你的问题,请参考以下文章