CouchDB文档建模原理

时间:2009-10-07 10:25:16

标签: couchdb modeling principles

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

您如何设计或分割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模型,与用户和标签相同。

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

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

您可以轻松地制作视图以找到您想要的内容。

那么我的问题是,你如何确定何时将文件分成较小的文件,或何时在文件之间建立“关系”?

我认为它会更加“面向对象”,并且更容易映射到Value Objects,如果它被划分为这样:


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

...但是它开始看起来更像是一个关系数据库。而且我经常会继承一些看似“文档中的整个站点”的东西,因此用关系对它进行建模会更加困难。

我已经阅读了很多关于如何/何时使用关系数据库与文档数据库的内容,因此这不是主要问题。我更想知道,在CouchDB中建模数据时应用什么是一个好的规则/原则。

另一个例子是XML文件/数据。一些XML数据已经嵌套了10个以上的层次,我想使用相同的客户端(例如Rails上的Ajax或Flex)可视化我将从ActiveRecord,CouchRest或任何其他对象关系映射器渲染JSON。有时我会得到整个站点结构的巨大XML文件,如下所示,我需要将它映射到Value Objects以在我的Rails应用程序中使用,因此我不必编写另一种序列化/反序列化数据的方法:

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

所以一般的CouchDB问题是:

  1. 您使用什么规则/原则来划分文件(关系等)?
  2. 将整个网站整合到一个文档中是否可以?
  3. 如果是这样,您如何处理具有任意深度级别的文档的序列化/反序列化(如上面的大型json示例或xml示例)?
  4. 或者你不把它们变成VO,你只是决定“这些太嵌套到Object-Relational Map,所以我只是使用原始XML / JSON方法访问它们”?
  5. 非常感谢您的帮助,如何将数据与CouchDB分开的问题我很难说“这是我应该从现在开始做的”。我希望很快能到达那里。

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

    1. Hierarchical Data in CouchDB
    2. CouchDB Wiki
    3. Sofa - CouchDB App
    4. CouchDB The Definitive Guide
    5. PeepCode CouchDB Screencast
    6. CouchRest
    7. CouchDB README
    8. ......但是他们仍然没有回答这个问题。

4 个答案:

答案 0 :(得分:25)

已经有了一些很好的答案,但是我想添加一些最近的CouchDB功能,以便与fortropos描述的原始情况一起使用。

分割文档的关键点是可能存在冲突的地方(如前所述)。您永远不应该将大量“纠结”的文档放在一个文档中,因为您将获得完全不相关的更新的单个修订路径(例如,添加注释添加整个站点文档的修订)。管理各种较小文档之间的关系或联系可能会让人感到困惑,但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", {}, {}]
甚至可以专门提取某个类型的列表:{{2这些查询参数组合是可能的,因为空对象(“{}”)始终位于排序规则的底部,而null或“”始终位于顶部。

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

?start_key=["123412804910820", "comment"]&end_key=["123412804910820", "comment", {}]
你的_list函数(在本例中命名为“unified”)将获取视图map / reduce的结果(在这种情况下)命名为“posts”)并通过JavaScript函数运行它们,该函数将以您需要的内容类型(JSON,HTML等)发回HTTP响应。

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

希望有所帮助。

答案 1 :(得分:15)

如果我没记错的话,book说,在“伤害”之前进行反规范化,同时记住文档可能更新的频率。

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

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

  
      
  1. 将整个网站放入一个文档是否可以?
  2.   

不,那会很愚蠢,因为:

  • 您必须在每次更新时读写整个网站(文档),效率非常低;
  • 您不会从任何视图缓存中受益。

答案 2 :(得分:15)

我知道这是一个老问题,但我遇到了它试图找出解决这个问题的最佳方法。 Christopher Lenz写了一篇关于methods of modeling "joins" in CouchDB的好文章。我的一个收获点是:“允许非冲突添加相关数据的唯一方法是将相关数据放入单独的文档中。”因此,为了简单起见,您希望倾向于“非规范化”。但是在某些情况下,由于写入冲突,你会遇到天然屏障。

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

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

答案 3 :(得分:5)

我认为Jake的回应指出了使用CouchDB最重要的一个方面,可能有助于你做出范围界定决定:冲突。

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

ASIDE:作为this article points out,还要考虑每次请求/更新该文档时,您必须完整地获取/设置文档,因此传递代表整个文档的大量文档网站或对其有很多评论的帖子可能会成为您想要避免的问题。

如果帖子与评论分开建模,两个人就故事提交评论,那么这些文件只会在该DB中成为两个“评论”文档,没有冲突问题;只需要两个PUT操作就可以在“comment”db中添加两条新注释。

然后,要编写回复帖子评论的视图,您将传递postID,然后发出引用该父帖子ID的所有注释,并按某种逻辑顺序排序。也许你甚至传递了诸如[postID,byUsername]之类的东西作为'评论'视图的关键字来指示父帖子以及你希望结果排序的方式或类似的东西。

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

由于Mongo的写锁定和单主属性质,两个人添加注释的冲突修订问题不会在那里出现,如上所述,内容的查询能力不会太差,因为分指数。

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