13

I'm trying to model a fairly simple relationship in CouchDB and I'm having trouble determining the best way to accomplish this. I'd like users to be able to create lists of video game objects. I have the video game documents stored in the DB already with "type":"game". I'd like to be able to query a list object's ID (via a view) and get back the list's metadata (title, creation date, etc.) and portions of the game document (such as title and release date). Furthermore, I'd like to be able to add/removes games to/from lists without downloading the entire list document and posting it back (so this means I can't simply store the game's information in the list document) as I'd eventually like to support multiple users contributing to the same list, and I don't want to introduce conflicts.

After reading the CouchDB wiki on EntityRelationships, I've determined that setting up relationship documents might be the best solution.

Game:

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

List:

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

Game-List Relationship:

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

But, from what I understand, this wouldn't allow me to get the list's metadata and the game's metadata in one request. Any advice?

4

1 回答 1

13

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

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

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

3.2.3。加入视图

3.2.3.1。链接文件

如果您的 map 函数发出一个对象值,{'_id': XXX}并且您使用参数查询视图include_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_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_idrelationship文档中指定的外键的指针。

分析:

  1. 您正在使用本质上不可变的文档来存储状态更改,并让数据库为您计算聚合状态。这是一个可爱的规模模型,也是我们最成功的模式之一。
  2. 添加或删除列表非常有效。
  3. 高并发下优秀的伸缩性
  4. 在 Cloudant(和 CouchDB v2.0),我们还没有二级索引的“读你写”一致性。它在优先级列表中很高,但存在潜在的极端情况,在故障场景或高负载下,您可能看不到主索引和辅助索引之间的立即一致性。长话短说,仲裁用于主索引,但仲裁不是二级索引的可行模型,因此正在开发另一种一致性策略。
于 2013-07-16T20:08:24.117 回答