3.2.3. 加入视图

3.2.3.1. 链接的文档

如果你 map function 发出具有 {{'_id': XXX}} 和你 query view 具有 include_docs=true 参数,则CouchDB将获取id为的文档 XXX 而不是处理为发出键/值对的文档。

这意味着,如果一个文档包含其他文档的id,那么也可以在视图中获取这些文档,如果需要,可以在同一个键附近获取这些文档。

例如,如果您有以下层次链接的文档:

[
    { "_id": "11111" },
    { "_id": "22222", "ancestors": ["11111"], "value": "hello" },
    { "_id": "33333", "ancestors": ["22222","11111"], "value": "world" }
]

在视图中,可以使用与它们相邻的祖先文档发出值,如下所示:

function(doc) {
    if (doc.value) {
        emit([doc.value, 0], null);
        if (doc.ancestors) {
            for (var i in doc.ancestors) {
                emit([doc.value, Number(i)+1], {_id: doc.ancestors[i]});
            }
        }
    }
}

结果是:

{
    "total_rows": 5,
    "offset": 0,
    "rows": [
        {
            "id": "22222",
            "key": [
                "hello",
                0
            ],
            "value": null,
            "doc": {
                "_id": "22222",
                "_rev": "1-0eee81fecb5aa4f51e285c621271ff02",
                "ancestors": [
                    "11111"
                ],
                "value": "hello"
            }
        },
        {
            "id": "22222",
            "key": [
                "hello",
                1
            ],
            "value": {
                "_id": "11111"
            },
            "doc": {
                "_id": "11111",
                "_rev": "1-967a00dff5e02add41819138abb3284d"
            }
        },
        {
            "id": "33333",
            "key": [
                "world",
                0
            ],
            "value": null,
            "doc": {
                "_id": "33333",
                "_rev": "1-11e42b44fdb3d3784602eca7c0332a43",
                "ancestors": [
                    "22222",
                    "11111"
                ],
                "value": "world"
            }
        },
        {
            "id": "33333",
            "key": [
                "world",
                1
            ],
            "value": {
                "_id": "22222"
            },
            "doc": {
                "_id": "22222",
                "_rev": "1-0eee81fecb5aa4f51e285c621271ff02",
                "ancestors": [
                    "11111"
                ],
                "value": "hello"
            }
        },
        {
            "id": "33333",
            "key": [
                "world",
                2
            ],
            "value": {
                "_id": "11111"
            },
            "doc": {
                "_id": "11111",
                "_rev": "1-967a00dff5e02add41819138abb3284d"
            }
        }
    ]
}

这使得在一个查询中获取一个文档及其所有祖先非常便宜。

请注意 "id" 行中仍然是原始文档的行。唯一的区别是 include_docs 获取不同的文档。

文档的当前修订版在查询时解析,而不是在生成视图时解析。这意味着,如果以后添加了链接文档的新修订,则即使视图本身没有更改,它也将出现在视图查询中。若要强制使用链接文档的特定修订,请发出 "_rev" 财产以及 "_id" .

3.2.3.2. 使用视图排序规则

作者:克里斯托弗·伦茨
日期:2007-10-05
来源:http://www.cmlenz.net/archives/2007/10/couchdb-joins

就在今天,在IRC上有一个关于如何用“post”和“comment”实体来建模一个简单的博客系统的讨论,其中任何一篇博客文章都可能有N条评论。如果使用SQL数据库,显然会有两个表带有外键,并且使用联接。(至少在你需要添加一些之前 denormalization

但是CouchDB中“显而易见”的方法是什么样子的呢?

3.2.3.2.1. 方法1:内联注释

一个简单的方法是每个博客文章有一个文档,并将评论存储在该文档中:

{
    "_id": "myslug",
    "_rev": "123456",
    "author": "john",
    "title": "My blog post",
    "content": "Bla bla bla …",
    "comments": [
        {"author": "jack", "content": "…"},
        {"author": "jane", "content": "…"}
    ]
}

注解

当然,一个实际的博客系统的模型会更广泛,你会有标签,时间戳等,这只是为了演示一些基本的东西。

这种方法的明显优点是属于一起的数据存储在一个地方。删除帖子,你会自动删除相应的评论,依此类推。

你可能会认为,把评论放在博客文章文档中不会让我们自己查询评论,但你错了。您可以简单地编写一个CouchDB视图,该视图将返回所有博客文章中的所有评论,并由作者键入:

function(doc) {
    for (var i in doc.comments) {
        emit(doc.comments[i].author, doc.comments[i].content);
    }
}

现在,您可以通过调用视图并将其传递给 ?key="username" 查询字符串参数。

但是,这种方法有一个缺点,对于许多应用程序来说可能非常重要:要向帖子添加评论,您需要:

  • 获取博客文章文档
  • 将新注释添加到JSON结构中
  • 将更新后的文档发送到服务器

现在,如果有多个客户机进程在大致相同的时间添加注释,其中一些进程将获得 HTTP 409 Conflict 步骤3出错(这是乐观并发操作)。对于某些应用程序来说,这是有意义的,但是在其他许多应用程序中,不管是否同时添加了其他数据,您都希望附加新的相关数据。

允许无冲突地添加相关数据的唯一方法是将相关数据放入单独的文档中。

3.2.3.2.2. 方法2:单独评论

使用这种方法,每个博客文章都有一个文档,每个评论都有一个文档。评论文档会有一个“反向链接”到他们所属的帖子。

除去comments属性,blog post文档看起来与上面类似。另外,我们现在在所有文档上都有一个type属性,这样我们就可以区分帖子和评论之间的区别了:

{
    "_id": "myslug",
    "_rev": "123456",
    "type": "post",
    "author": "john",
    "title": "My blog post",
    "content": "Bla bla bla …"
}

注释本身存储在单独的文档中,这些文档还具有一个类型属性(这次的值为“comment”),另外还有一个post属性,其中包含它们所属的post文档的ID:

{
    "_id": "ABCDEF",
    "_rev": "123456",
    "type": "comment",
    "post": "myslug",
    "author": "jack",
    "content": "…"
}
{
    "_id": "DEFABC",
    "_rev": "123456",
    "type": "comment",
    "post": "myslug",
    "author": "jane",
    "content": "…"
}

要列出每个博客文章的所有评论,您需要添加一个简单的视图,由blog post ID键控:

function(doc) {
    if (doc.type == "comment") {
        emit(doc.post, {author: doc.author, content: doc.content});
    }
}

你可以调用这个视图,传递一个 ?key="post_id" 查询字符串参数。

按作者查看所有评论和以前一样简单:

function(doc) {
    if (doc.type == "comment") {
        emit(doc.author, {post: doc.post, content: doc.content});
    }
}

所以这在某些方面更好,但也有缺点。假设您想要在同一个网页上显示一篇包含所有相关评论的博客文章。对于第一种方法,我们只需要对CouchDB服务器发出一个请求,即 GET 对文档的请求。对于第二种方法,我们需要两个请求:a GET 对post文档的请求,以及 GET 请求返回帖子的所有评论的视图。

可以,但不太令人满意。想象一下你想要添加线程化的注释:你现在需要为每个注释添加一个fetch。我们可能想要的是一种将博客文章和各种评论连接在一起的方法,以便能够通过单个HTTP请求检索它们。

就在这时,CouchDB的作者damienkatz加入了关于IRC的讨论,为我们指明了方向。

3.2.3.2.3. 优化:使用视图排序功能

对Damien来说是显而易见的,但对我们其他人来说却一点也不明显:创建一个包含博客帖子文档内容和与该帖子相关的所有评论内容的视图非常简单。你这样做的方法是使用 complex keys . 到目前为止,我们一直在为视图键使用简单的字符串值,但实际上它们可以是任意的JSON值,所以让我们利用一下:

function(doc) {
    if (doc.type == "post") {
        emit([doc._id, 0], null);
    } else if (doc.type == "comment") {
        emit([doc.post, 1], null);
    }
}

好吧,一开始可能会让人困惑。让我们退一步看看CouchDB中的视图到底是关于什么的。

CouchDB视图在将键映射到值的磁盘字典上基本上是高效的,其中键被自动索引,并可用于过滤和/或排序从视图返回的结果。当您“调用”视图时,可以通过指定 ?key=foo 查询字符串参数。或者你可以指定 ?startkey=foo 和/或 ?endkey=bar 查询字符串参数以获取键范围内的行。最后,通过添加 ?include_docs=true 对于查询,结果将包括每个发出的文档的完整正文。

还需要注意的是,键总是用于对行进行排序(即排序)。CouchDB有很好的定义(但目前还没有文档化)规则来比较任意JSON对象进行排序。例如,JSON值 ["foo", 2] 在值之后排序(视为“大于”) ["foo"]["foo", 1, "bar"] ,但在。 ["foo", 2, "bar"] . 这个功能可以实现一系列相当不明显的技巧。。。

参见

视图排序

记住这一点,让我们回到上面的view函数。首先要注意的是,与我们在这里使用的前面的视图函数不同,这个视图同时处理“post”和“comment”文档,它们最终都是同一个视图中的行。另外,这个视图中的键不仅仅是一个简单的字符串,而是一个数组。该数组中的第一个元素始终是post的ID,而不管我们处理的是实际的post文档,还是与post相关联的注释。第二个元素是0,表示post文档,1表示注释文档。

假设数据库中有两篇博客文章。在不限制查看结果的情况下,通过 keystartkeyendkey ,我们会得到如下信息:

{
    "total_rows": 5, "offset": 0, "rows": [{
            "id": "myslug",
            "key": ["myslug", 0],
            "value": null
        }, {
            "id": "ABCDEF",
            "key": ["myslug", 1],
            "value": null
        }, {
            "id": "DEFABC",
            "key": ["myslug", 1],
            "value": null
        }, {
            "id": "other_slug",
            "key": ["other_slug", 0],
            "value": null
        }, {
            "id": "CDEFAB",
            "key": ["other_slug", 1],
            "value": null
        },
    ]
}

注解

这个 ... 这里的占位符将包含相应文档的完整JSON编码

现在,要获取特定的博客文章和所有相关的评论,我们将使用查询字符串调用该视图:

?startkey=["myslug"]&endkey=["myslug", 2]&include_docs=true

我们会找回前三排,那些属于 myslug 张贴,但不包括其他文件,以及每个文件的正文。等等,我们现在有了数据,我们需要显示一个帖子,所有相关的评论,检索通过一个单一的 GET 请求。

您可能会问,键的0和1部分是用来做什么的。它们只是为了确保post文档总是在关联的注释文档之前排序。因此,当您从该视图中获取特定帖子的结果时,您将知道第一行包含博客文章本身的数据,其余行包含评论数据。

这个模型的一个遗留问题是注释没有被排序,但这只是因为我们没有与它们相关联的日期/时间信息。如果有,我们将添加时间戳作为键数组的第三个元素,可能是ISO日期/时间字符串。现在我们将继续使用查询字符串 ?startkey=["myslug"]&endkey=["myslug", 2]&include_docs=true 为了获取博客文章和所有相关的评论,只有现在它们才按时间顺序排列。