在CouchDB中建模文档层次结构的最佳方法

时间:2013-04-24 09:35:51

标签: couchdb hierarchical-data

我正在尝试在CouchDB中为文档构建一个层次结构,以便在我的系统中使用,这在概念上类似于博客。每篇博文都属于至少一个类别,每个类别可以有很多帖子。类别是分层的,这意味着如果帖子属于层次结构中的 CatB CatA - > CatB ”(“CatB在CatA中) “,它也属于 CatA

用户必须能够快速查找某个类别(及其所有子女)中的所有帖子。

解决方案1 ​​ 帖子类型的每个文档都包含一个“类别”数组,表示它在层次结构中的位置(参见2)。

{
   "_id": "8e7a440862347a22f4a1b2ca7f000e83",
   "type": "post",
   "author": "dexter",
   "title": "Hello",
   "category":["OO","Programming","C++"]
}

解决方案2 post类型的每个文档都包含表示层次结构中路径的“category”字符串(请参阅4)。

{
   "_id": "8e7a440862347a22f4a1b2ca7f000e83",
   "type": "post",
   "author": "dexter",
   "title": "Hello",
   "category": "OO/Programming/C++"
}

解决方案3 post类型的每个文档都包含其父类“category”id,表示其在层次结构中的路径(请参阅3)。通过链接的“类别”文档类型构建分层类别结构。

{
   "_id": "8e7a440862347a22f4a1b2ca7f000e83",
   "type": "post",
   "author": "dexter",
   "title": "Hello",
   "category_id": "3"
}

{
   "_id": "1",
   "type": "category",
   "name": "OO"
}


{
   "_id": "2",
   "type": "category",
   "name": "Programming",
   "parent": "1"
}


{
   "_id": "3",
   "type": "category",
   "name": "C++",
   "parent": "2"
}

问题

在CouchDB中存储此类关系的最佳方式是什么?在磁盘空间,可伸缩性和检索速度方面,最有效的解决方案是什么?

这样的关系是否可以建模以考虑本地化的类别名称?

声明

我知道这个问题已经在SO上被问了好几次,但似乎没有明确的答案,也没有一个答案可以解决每个解决方案的优缺点。对不起问题的长度:)

到目前为止阅读

CouchDB - The Definitive Guide

Storing Hierarchical Data in CouchDB

Retrieving Hierarchical/Nested Data From CouchDB

Using CouchDB group_level for hierarchical data

1 个答案:

答案 0 :(得分:24)

这个问题没有正确的答案,因此缺乏确定的答案。它主要取决于您想要优化的用途。

您声明属于特定类别(及其子级)的文档的检索速度最重要。前两个解决方案允许您创建一个多次发布博客帖子的视图,一次针对从叶子到根的链中的每个类别。因此,可以使用单个(因此快速)查询来完成选择所有文档。第二个解决方案与第一个解决方案的唯一区别在于,您将类别“路径”的解析移动到将文档插入视图的 map 函数的代码中的组件中。我更喜欢第一种解决方案,因为它更容易实现 map 函数并且更加灵活(例如,它允许类别的名称包含斜杠字符)。

在您的方案中,您可能还想创建一个缩小视图,该视图计算每个类别的博客帖子数。使用这些解决方案中的任何一个都非常简单。使用拟合缩减功能,可以使用单个请求检索每个类别中的帖子数量。

前两个解决方案的缺点是重命名或将类别从一个父级移动到另一个父级需要更新每个文档。第三种解决方案允许不触摸文档。但是根据您的场景描述,我假设按类别检索是非常频繁的,并且类别重命名/移动非常罕见。

解决方案4 我建议第四个解决方案,其中博客帖子文档包含对类别文档的引用,但仍然引用帖子类别的所有祖先。这样可以在不触及博客帖子的情况下重命名类别,并允许您存储带有类别的其他元数据(例如,类别名称或说明的翻译):

{
    "_id": "8e7a440862347a22f4a1b2ca7f000e83",
    "type": "post",
    "author": "dexter",
    "title": "Hello",
    "category_ids": [3, 2, 1]
}

{
    "_id": "1",
    "type": "category",
    "name": "OO"
}

{
    "_id": "2",
    "type": "category",
    "name": "Programming",
    "parent": "1"
}


{
    "_id": "3",
    "type": "category",
    "name": "C++",
    "parent": "2"
}

您仍然必须存储类别的父类别,这些类别会在帖子中复制数据,以允许遍历类别(例如,用于显示导航类别树)。

您可以扩展此解决方案或任何解决方案,以便将帖子分类到多个类别下,或者将类别分类为多个父母。当帖子被分类为多个类别时,您需要在帖子的文档中存储每个类别的祖先的联合,同时保留作者选择的类别,以允许它们与帖子一起显示或稍后编辑。

让我们假设还有一个名为“Ajax”的附加类别,其中包含anchestors“JavaScript”,“Programming”和“OO”。为了简化以下示例,我选择了类别的文档ID以等于类别的名称。

{
    "_id": "8e7a440862347a22f4a1b2ca7f000e83",
    "type": "post",
    "author": "dexter",
    "title": "Hello",
    "category_ids": ["C++", "Ajax"],
    "category_anchestor_ids": ["C++", "Programming", "OO", "Ajax", "JavaScript"]
}

要允许某个类别拥有多个父级,只需存储具有类别的多个父级ID。在查找某个类别的所有祖先时,您需要消除重复项。

查看解决方案4 假设您要获取特定类别的所有博客帖子。我们将使用包含以下示例数据的数据库:

{ "_id": "100", "type": "category", "name": "OO"                              }
{ "_id": "101", "type": "category", "name": "Programming", "parent_id": "100" }
{ "_id": "102", "type": "category", "name": "C++",         "parent_id": "101" }
{ "_id": "103", "type": "category", "name": "JavaScript",  "parent_id": "101" }
{ "_id": "104", "type": "category", "name": "AJAX",        "parent_id": "103" }

{ "_id": "200", "type": "post", "title": "OO Post",          "category_id": "104", "category_anchestor_ids": ["100"]                      }
{ "_id": "201", "type": "post", "title": "Programming Post", "category_id": "101", "category_anchestor_ids": ["101", "100"]               }
{ "_id": "202", "type": "post", "title": "C++ Post",         "category_id": "102", "category_anchestor_ids": ["102", "101", "100"]        }
{ "_id": "203", "type": "post", "title": "AJAX Post",        "category_id": "104", "category_anchestor_ids": ["104", "103", "101", "100"] }

除此之外,我们在名为posts_by_category的设计文档中使用名为_design/blog的视图,其中包含以下 map 函数:

function (doc) {
    if (doc.type == 'post') {
        for (i in doc.category_anchestor_ids) {
            emit([doc.category_anchestor_ids[i]], doc)
        }
    }
}

然后,我们可以使用Programming对以下网址的请求,获取"101"类别(ID为GET)或其中一个子类别的所有帖子。

http://localhost:5984/so/_design/blog/_view/posts_by_category?reduce=false&key=["101"]

这将返回一个视图结果,其中键设置为类别ID,并且值设置为发布文档。同样的视图也可用于获取所有类别的摘要列表以及该类别中的帖子数量及其子项。我们将以下 reduce 函数添加到视图中:

function (keys, values, rereduce) {
    if (rereduce) {
        return sum(values)
    } else {
        return values.length
    }
}

然后我们使用以下网址:

http://localhost:5984/so/_design/blog/_view/posts_by_category?group_level=1

这将返回缩小的视图结果,其中键再次设置为类别ID,值设置为每个类别中的帖子数。在此示例中,必须单独获取类别名称,但可以创建视图,其中简化视图结果中的每一行都已包含类别名称。