3.2.3. 使用视图进行连接

3.2.3.1. 关联文档

如果您的 映射函数 发射一个包含 {'_id': XXX} 的对象值,并且您 查询视图 使用 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" 仍然是源文档的 ID。唯一的区别是 include_docs 获取的是不同的文档。

文档的当前修订版是在查询时解析的,而不是在生成视图时解析的。这意味着,如果稍后添加了关联文档的新修订版,它将出现在视图查询中,即使视图本身没有改变。要强制使用关联文档的特定修订版,请同时发射 "_rev" 属性和 "_id" 属性。

3.2.3.2. 使用视图排序

作者:

Christopher Lenz

日期:

2007-10-05

来源:

http://www.cmlenz.net/archives/2007/10/couchdb-joins

就在今天,IRC 上有人讨论如何用“帖子”和“评论”实体来建模一个简单的博客系统,其中任何博客帖子都可能拥有 N 个评论。如果您使用的是 SQL 数据库,您显然会有两个带有外键的表,并且您会使用连接。(至少在您需要添加一些 反规范化 之前)。

但是 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 结构中

  • 将更新后的文档发送到服务器

现在,如果您有多个客户端进程在几乎相同的时间添加评论,其中一些进程将在步骤 3 中收到 HTTP 409 冲突 错误(这就是乐观并发在起作用)。对于某些应用程序来说,这是有意义的,但在许多其他应用程序中,您希望无论其他数据是否在同时添加,都追加新的相关数据。

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

3.2.3.2.2. 方法 #2:单独的评论

使用这种方法,您每个博客帖子有一个文档,每个评论有一个文档。评论文档将有一个指向其所属帖子的“反向链接”。

博客帖子文档将与上面类似,只是缺少评论属性。此外,我们现在将在所有文档上添加一个类型属性,以便我们可以区分帖子和评论

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

评论本身存储在单独的文档中,这些文档也具有类型属性(这次值为“评论”),此外还具有一个帖子属性,该属性包含其所属帖子文档的 ID

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

要列出每个博客帖子的所有评论,您需要添加一个简单的视图,并按博客帖子 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 请求到该文档。使用第二种方法,我们需要两个请求:一个 GET 请求到帖子文档,以及一个 GET 请求到返回帖子所有评论的视图。

这还可以,但并不完全令人满意。想象一下,您想添加嵌套评论:现在您需要为每个评论添加一个额外的获取操作。然后,我们可能想要的是一种方法,将博客帖子和各种评论连接在一起,以便能够通过单个 HTTP 请求检索它们。

就在这时,CouchDB 的作者 Damien Katz 在 IRC 上的讨论中插话,向我们指明了方向。

3.2.3.2.3. 优化:利用视图排序的力量

对 Damien 来说很明显,但对我们其他人来说却一点也不明显:创建一个包含博客帖子文档内容和与该帖子关联的所有评论内容的视图相当简单。您实现此目的的方法是使用 复杂键。到目前为止,我们一直在为视图键使用简单的字符串值,但实际上它们可以是任意的 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"] 之前。这个特性使一整类技巧成为可能,这些技巧相当不明显……

另请参阅

视图排序

考虑到这一点,让我们回到上面的视图函数。首先要注意,与我们之前使用过的视图函数不同,此视图处理“post”和“comment”两种文档,并且它们最终都作为同一视图中的行。此外,此视图中的键不仅仅是一个简单的字符串,而是一个数组。该数组中的第一个元素始终是帖子的 ID,无论我们是在处理实际的帖子文档还是与帖子关联的评论。第二个元素对于帖子文档为 0,对于评论文档为 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 部分的作用。它们只是为了确保帖子文档始终在关联的评论文档之前排序。因此,当您从该视图中获取特定帖子的结果时,您将知道第一行包含博客帖子本身的数据,其余行包含评论数据。

此模型中剩下的一个问题是评论没有排序,但这仅仅是因为我们没有与它们关联的日期/时间信息。如果我们有,我们将时间戳作为键数组的第三个元素添加,可能作为 ISO 日期/时间字符串。现在我们将继续使用查询字符串 ?startkey=["myslug"]&endkey=["myslug", 2]&include_docs=true 来获取博客帖子及其所有关联评论,只是现在它们将按时间顺序排列。