在CouchDB上建模文档之间的关系?

在CouchDB上建模文档之间的关系?,couchdb,cloudant,Couchdb,Cloudant,我试图在CouchDB中建立一个相当简单的关系模型,但我很难确定实现这一点的最佳方法。我希望用户能够创建视频游戏对象的列表。我已经将视频游戏文档存储在数据库中,并带有“type”:“game”。我希望能够查询列表对象的ID(通过视图),并返回列表的元数据(标题、创建日期等)和游戏文档的部分(例如标题和发布日期)。此外,我希望能够在列表中添加/删除游戏,而无需下载整个列表文档并将其发回(因此这意味着我不能简单地将游戏信息存储在列表文档中),因为我最终希望支持多个用户参与同一列表,并且我不希望引入冲

我试图在CouchDB中建立一个相当简单的关系模型,但我很难确定实现这一点的最佳方法。我希望用户能够创建视频游戏对象的列表。我已经将视频游戏文档存储在数据库中,并带有
“type”:“game”
。我希望能够查询列表对象的ID(通过视图),并返回列表的元数据(标题、创建日期等)和游戏文档的部分(例如标题和发布日期)。此外,我希望能够在列表中添加/删除游戏,而无需下载整个列表文档并将其发回(因此这意味着我不能简单地将游戏信息存储在列表文档中),因为我最终希望支持多个用户参与同一列表,并且我不希望引入冲突

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

游戏: 名单: 游戏列表关系:
但是,据我所知,这不允许我在一个请求中获取列表元数据和游戏元数据。有什么建议吗?

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

  • 用户列表和游戏之间存在多对多关系
  • 一对多关系很容易在单个文档中表示,该文档使用容器来表示“多”部分,但它们会变大,并且可能存在并发冲突
  • 扩展单文档模型以存储多对多关系是不可行的
  • 一般来说,文档不变性非常适合并发系统。在CouchDB中,您可以完全按照您所说的那样,通过在图形中存储表示边缘的“写一次”文档,然后使用二级索引来重建所需链接的部分,并在单个API查询调用中获取所需信息
  • 你也说得对,这里的解决方案是“地图端连接”(借用hadoop社区)。基本上,您希望在地图输出中使用不同的行来表示不同的信息。然后,您可以使用范围查询(startkey/endkey)来查询所需的映射结果部分,以及“join”表的物化视图。然而,您在文档中没有找到的一个难题是:

    3.2.3. 3.2.3.1. 链接文档 如果map函数发出一个具有
    {u id':XXX}
    的对象值,并且您使用
    include\u docs=true
    参数查询视图,那么CouchDB将获取id为
    XXX
    的文档,而不是经过处理以发出键/值对的文档

    这就说明了一切。这就是如何取消引用指向通过外键存储的链接文档的指针。然后将其与复合键(JS数组键)的使用和

    这样,您的视图行将按如下方式排序:

    ["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\u 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
    选项将跟随指向您在
    关系
    文档中用
    游戏id
    指定的外键的指针

    分析:

  • 您使用本质上不可变的文档来存储状态更改,并让数据库为您计算聚合状态。这是一个可爱的规模模型,也是我们最成功的模式之一
  • 非常有效地添加或删除列表
  • 在高并发性下具有优异的扩展性能
  • 在Cloudant(和CouchDB v2.0)中,我们还没有“二级索引的读写一致性”。它在优先级列表中很高,但也有一些潜在的情况,在故障场景或高负载中,您可能看不到主索引和辅助索引之间的即时一致性。长话短说,quorum用于主索引,但quorum不是用于辅助索引的可行模型,因此正在开发另一种一致性策略

  • if(doc.type==“list”){
    中的emit不应该是
    emit(doc.u id,doc)
    ?或者甚至是
    emit([doc.u id,0],null)
    ?我只是有点搞不清楚你的目的是什么,因为有一个不成对的括号和一个没有第二个值的逗号。从视图输出
    [“list\u 1”],null
    ,我会选择
    emit([doc.\u id],null)
    。是的,我在这里有点草率。有几件事需要注意。(1)键的结构(emit()方法中的firs参数不需要在每行之间完全相同。这很有用。例如,[“foo”]将在[“foo”,“foo”]之前排序。(2)如果您有一个空条目(例如emit(“foo”);JS运行时将自动在其位置插入
    null
    。抱歉,太草率了。
    ["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});
        }
    }   
    
    curl -g 'https://usr:pwd@usr.cloudant.com/db/_design/design_doc_name/_view/view_name?startkey=["123"]&endkey=["123",{}]&include_docs=true'