125

我有一个问题,我一直试图回答一段时间,但无法弄清楚:

您如何设计或分割 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 文档中提取评论的视图,以将它们放入评论模型中,与用户和标签相同。

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


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 问题是:

  1. 您使用什么规则/原则来划分文件(关系等)?
  2. 可以将整个网站放在一个文档中吗?
  3. 如果是这样,您如何处理具有任意深度级别的序列化/反序列化文档(如上面的大型 json 示例或 xml 示例)?
  4. 或者你不把它们变成 VO,你只是决定“这些太嵌套到对象关系映射,所以我将使用原始 XML/JSON 方法访问它们”?

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

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

  1. CouchDB 中的分层数据
  2. CouchDB 维基
  3. 沙发 - CouchDB 应用程序
  4. CouchDB 权威指南
  5. PeepCode CouchDB 截屏视频
  6. 沙发休息
  7. CouchDB 自述文件

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

4

4 回答 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”(在本例中为“发布”文档)。请注意,这些结果将包含一个“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 函数运行它们,该函数将以您的内容类型发回 HTTP 响应需要(JSON、HTML 等)。

结合这些内容,您可以将文档拆分为您认为对更新、冲突和复制有用且“安全”的任何级别,然后在需要时将它们重新组合在一起。

希望有帮助。

于 2011-09-30T13:55:54.980 回答
17

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

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

所以我认为经验法则是“非规范化直到它受伤”,但它会“受伤”的地方是你很有可能针对文档的同一修订发布多次编辑。

于 2011-02-17T23:33:11.707 回答
16

本书说,如果我没记错的话,去规范化直到“痛”,同时记住你的文档可能更新的频率。

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

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

  1. 可以将整个网站放在一个文档中吗?

不,那太愚蠢了,因为:

  • 您必须在每次更新时读取和写入整个站点(文档),这是非常低效的;
  • 您不会从任何视图缓存中受益。
于 2009-10-07T11:10:53.480 回答
6

我认为 Jake 的回答指出了与 CouchDB 合作的最重要方面之一,这可能有助于您做出范围界定决定:冲突。

如果您将评论作为帖子本身的数组属性,并且您只有一个“帖子”数据库,其中包含一堆巨大的“帖子”文档,正如 Jake 和其他人正确指出的那样,您可以想象一个场景一个非常受欢迎的博客文章,其中两个用户同时提交对文章文档的编辑,导致该文档发生冲突和版本冲突。

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

在帖子与评论分开建模并且两个人对故事提交评论的情况下,这些只是成为该数据库中的两个“评论”文档,没有冲突问题;只需两个 PUT 操作即可将两个新评论添加到“评论”数据库。

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

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

由于 Mongo 的写入锁定和单主特性,不会出现两个人添加评论的冲突修订问题,并且如上所述,内容的查询能力不会因为 sub- 而影响太差。索引。

话虽这么说,如果您在任一数据库中的子元素都很大(比如成千上万条评论),我相信两个阵营都建议制作这些单独的元素;我当然已经看到 Mongo 就是这种情况,因为文档及其子元素的大小有一些上限。

于 2011-09-17T02:02:47.110 回答