【问题标题】:Modeling relationships on CouchDB between documents?在 CouchDB 上建模文档之间的关系?
【发布时间】:2013-07-15 02:23:46
【问题描述】:

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

阅读 EntityRelationships 上的 CouchDB wiki 后,我确定设置关系文档可能是最佳解决方案。

游戏:

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

列表:

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

游戏列表关系:

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

但是,据我了解,这不允许我在一个请求中获取列表的元数据和游戏的元数据。有什么建议吗?

【问题讨论】:

    标签: couchdb cloudant


    【解决方案1】:

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

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

    您也说得对,这里的解决方案是“map-side-join”(借用 hadoop 社区)。基本上,您希望在地图输出中使用不同的行来表示不同的信息。然后,您可以使用范围查询(startkey/endkey)来查询您需要的映射结果部分,以及,瞧,您的“连接”表的物化视图。但是,您在文档中没有找到的一个难题是:

    3.2.3。 Joins With Views

    3.2.3.1。链接文档

    如果您的地图函数发出一个具有{'_id': XXX} 的对象值并且您使用include_docs=true 参数查询视图,那么CouchDB 将获取ID 为XXX 的文档,而不是被处理以发出键/值的文档对。

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

    以便我们对您的视图行进行排序:

    ["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)中,我们还没有二级索引的“读你写”一致性。它在优先级列表中很高,但存在潜在的极端情况,在故障场景或高负载下,您可能看不到主索引和辅助索引之间的立即一致性。长话短说,仲裁用于主索引,但仲裁不是二级索引的可行模型,因此正在开发另一种一致性策略。

    【讨论】:

    • if (doc.type=="list") { 中的发射不应该是emit(doc._id,doc)吗?
    • 甚至emit([doc._id, 0], null)?我只是有点困惑你的目标是什么,因为有一个不成对的括号和一个没有第二个值的逗号。
    • 从视图输出["list_1"], null,我会选择emit([doc._id], null)。
    • 是的,我在这里有点马虎。有几点需要注意。 (1) 键的结构(emit() 方法中的第一个参数不需要逐行相同。这很有用。例如,["foo"] 将在 ["foo","foo 之前排序"]. (2) 如果你有一个空白条目(例如 emit("foo",); JS 运行时会自动在它的位置插入null。抱歉马虎了。
    猜你喜欢
    • 1970-01-01
    • 1970-01-01
    • 2016-04-03
    • 1970-01-01
    • 2010-12-04
    • 2013-03-07
    • 1970-01-01
    • 2019-02-26
    • 1970-01-01
    相关资源
    最近更新 更多