问题 建模文件之间的CouchDB关系?


我正在尝试在CouchDB中建立一个相当简单的关系,我无法确定实现这一目标的最佳方法。我希望用户能够创建视频游戏对象列表。我已将数据存储在DB中的视频游戏文档 "type":"game"。我希望能够查询列表对象的ID(通过视图)并获取列表的元数据(标题,创建日期等)和游戏文档的部分(例如标题和发布日期)。此外,我希望能够在列表中添加/删除游戏,而无需下载整个列表文档并将其发回(因此这意味着我不能简单地将游戏的信息存储在列表文档中)最终喜欢支持多个用户贡献同一个列表,我不想引入冲突。

在阅读了CouchDB wiki之后 EntityRelationships,我已经确定设置关系文档可能是最好的解决方案。

游戏:

{
    "_id": "2600emu",
    "type": "game"
}

列表:

{
    "_id": 123,
    "title": "Emulators",
    "user_id": "dstaley",
    "type": "list"
}

游戏列表关系:

{
    "_id": "98765456789876543",
    "type": "relationship",
    "list_id": 123,
    "game_id": "2600emu"
}

但是,根据我的理解,这不允许我获取列表的元数据和游戏的元数据 一个请求。任何建议?


4940
2017-07-16 18:37


起源



答案:


好问题。您确定了一些非常重要的原因,即使用“规范化”数据模型(带链接的不同文档类型)是最佳模型:

  1. 用户<==>列出<==>游戏之间存在多对多关系。
  2. 一对多关系很容易在单个文档中表示,该文档使用容器用于“多个”部分,但它们变大并且您可能会出现并发冲突。
  3. 扩展单文档模型以存储多对多关系是站不住脚的。
  4. 通常,文档不变性非常适合并发系统。在CouchDB中,您可以通过存储表示图中边缘的“一次写入”文档,然后使用二级索引重建所需的链接部分并在单个链接中获取所需信息来完全按照您的指示执行此操作。 API查询调用。

你也是对的,这里的解决方案是'map-side-join'(借用hadoop社区)。基本上,您希望在地图输出中使用不同的行来表示不同的信息。然后,您可以使用范围查询(startkey / endkey)来查询所需的地图结果部分,并且,瞧,“连接”表的物化视图。但是,你在文档中找不到的一块拼图是这样的:

http://wiki.apache.org/couchdb/Introduction_to_CouchDB_views#Linked_documents

那里的第一行:

“如果您发出的对象值为{'_ id':XXX},则include_docs = true将获取ID为XXX的文档,而不是处理为发出键/值对的文档。”

说了这一切。这就是你取消引用你通过外键存储的链接文档的指针的方法。然后,将其与使用复合键(JS数组的键)和视图聚合规则相结合:

http://wiki.apache.org/couchdb/View_collat​​ion?action=show&redirect=ViewCollat​​ion#Collat​​ion_Specification

这样我们的视图行就像:

["list_1"], null
["list_1", "game"], {"_id":"game_1234"}
["list_1", "game"], {"_id":"game_5678"}
["list_2"], null
["list_2","game"], {"_id":"game1234"}
["list_3"], null
...

将此与您现有的数据模型放在一起,这里有一些(未经测试的)伪代码应该可以解决这个问题:

function(doc) {
    if (doc.type=="list") {
        //this is the one in the one-to-many
        emit( [doc._id]),);
    }
    else if (doc.type=="relationship") {
        //this is the many in the one-to-many
        //doc.list_id is our foreign key to the list.  We use that as the key
        //doc.game_id is the foreign key to the game.  We use that as the value
        emit( [doc.list_id,'game'],  {'_id': doc.game_id});
    }
}   

最后,您将使用startkey / endkey查询,以便获得以您感兴趣的list_id开头的所有行。它看起来像:

curl -g 'https://usr:pwd@usr.cloudant.com/db/_design/design_doc_name/_view/view_name?startkey=["123"]&endkey=["123",{}]&include_docs=true'

-g 选项告诉curl不要为glob,这意味着你不必取消引用你的方括号等等 include_docs=true 选项将跟随指向您指定的外键的指针 game_id 在里面 relationship 文件。

分析:

  1. 您正在使用基本上不可变的文档来存储状态更改,并让数据库为您计算聚合状态。这是一个可爱的大型模型,也是我们最成功的模式之一。
  2. 对列表的添加或删除非常有效。
  3. 高并发性下的出色扩展属性
  4. 在Cloudant(和CouchDB v2.0)中,我们还没有“二次索引的读写一致性”。它在优先级列表中很高,但是存在潜在的极端情况,在故障情况或高负载情况下,您可能看不到主索引和辅助索引之间的直接一致性。长话短说,仲裁用于主要索引,但仲裁不是二级索引的可行模型,因此正在开发另一种一致性策略。

12



不应该排放在 if (doc.type=="list") { 是 emit(doc._id,doc)? - dstaley
甚至 emit([doc._id, 0], null)?我只是有点困惑你的目标,因为有一个不成对的括号和一个没有第二个值的逗号。 - dstaley
从视图输出 ["list_1"], null,我会去 emit([doc._id], null)。 - Mike Rhodes
是的,我在这里有点草率。有几点需要注意。 (1)关键的结构(第一个论点 emit() 方法不需要在行与行之间相同。这很有用。例如,[“foo”]将在[“foo”,“foo”]之前排序。 (2)如果你有一个空白条目(例如emit(“foo”,)); JS运行时将自动插入 null 在它的位置。抱歉是马虎。 - Mike Miller


答案:


好问题。您确定了一些非常重要的原因,即使用“规范化”数据模型(带链接的不同文档类型)是最佳模型:

  1. 用户<==>列出<==>游戏之间存在多对多关系。
  2. 一对多关系很容易在单个文档中表示,该文档使用容器用于“多个”部分,但它们变大并且您可能会出现并发冲突。
  3. 扩展单文档模型以存储多对多关系是站不住脚的。
  4. 通常,文档不变性非常适合并发系统。在CouchDB中,您可以通过存储表示图中边缘的“一次写入”文档,然后使用二级索引重建所需的链接部分并在单个链接中获取所需信息来完全按照您的指示执行此操作。 API查询调用。

你也是对的,这里的解决方案是'map-side-join'(借用hadoop社区)。基本上,您希望在地图输出中使用不同的行来表示不同的信息。然后,您可以使用范围查询(startkey / endkey)来查询所需的地图结果部分,并且,瞧,“连接”表的物化视图。但是,你在文档中找不到的一块拼图是这样的:

http://wiki.apache.org/couchdb/Introduction_to_CouchDB_views#Linked_documents

那里的第一行:

“如果您发出的对象值为{'_ id':XXX},则include_docs = true将获取ID为XXX的文档,而不是处理为发出键/值对的文档。”

说了这一切。这就是你取消引用你通过外键存储的链接文档的指针的方法。然后,将其与使用复合键(JS数组的键)和视图聚合规则相结合:

http://wiki.apache.org/couchdb/View_collat​​ion?action=show&redirect=ViewCollat​​ion#Collat​​ion_Specification

这样我们的视图行就像:

["list_1"], null
["list_1", "game"], {"_id":"game_1234"}
["list_1", "game"], {"_id":"game_5678"}
["list_2"], null
["list_2","game"], {"_id":"game1234"}
["list_3"], null
...

将此与您现有的数据模型放在一起,这里有一些(未经测试的)伪代码应该可以解决这个问题:

function(doc) {
    if (doc.type=="list") {
        //this is the one in the one-to-many
        emit( [doc._id]),);
    }
    else if (doc.type=="relationship") {
        //this is the many in the one-to-many
        //doc.list_id is our foreign key to the list.  We use that as the key
        //doc.game_id is the foreign key to the game.  We use that as the value
        emit( [doc.list_id,'game'],  {'_id': doc.game_id});
    }
}   

最后,您将使用startkey / endkey查询,以便获得以您感兴趣的list_id开头的所有行。它看起来像:

curl -g 'https://usr:pwd@usr.cloudant.com/db/_design/design_doc_name/_view/view_name?startkey=["123"]&endkey=["123",{}]&include_docs=true'

-g 选项告诉curl不要为glob,这意味着你不必取消引用你的方括号等等 include_docs=true 选项将跟随指向您指定的外键的指针 game_id 在里面 relationship 文件。

分析:

  1. 您正在使用基本上不可变的文档来存储状态更改,并让数据库为您计算聚合状态。这是一个可爱的大型模型,也是我们最成功的模式之一。
  2. 对列表的添加或删除非常有效。
  3. 高并发性下的出色扩展属性
  4. 在Cloudant(和CouchDB v2.0)中,我们还没有“二次索引的读写一致性”。它在优先级列表中很高,但是存在潜在的极端情况,在故障情况或高负载情况下,您可能看不到主索引和辅助索引之间的直接一致性。长话短说,仲裁用于主要索引,但仲裁不是二级索引的可行模型,因此正在开发另一种一致性策略。

12



不应该排放在 if (doc.type=="list") { 是 emit(doc._id,doc)? - dstaley
甚至 emit([doc._id, 0], null)?我只是有点困惑你的目标,因为有一个不成对的括号和一个没有第二个值的逗号。 - dstaley
从视图输出 ["list_1"], null,我会去 emit([doc._id], null)。 - Mike Rhodes
是的,我在这里有点草率。有几点需要注意。 (1)关键的结构(第一个论点 emit() 方法不需要在行与行之间相同。这很有用。例如,[“foo”]将在[“foo”,“foo”]之前排序。 (2)如果你有一个空白条目(例如emit(“foo”,)); JS运行时将自动插入 null 在它的位置。抱歉是马虎。 - Mike Miller