MongoDB关系:嵌入还是引用?


524

我是MongoDB的新手-来自关系数据库背景。我想设计一个带有一些注释的问题结构,但是我不知道该使用哪种关系进行注释:embed或者reference

一个带有一些注释的问题,例如stackoverflow,将具有以下结构:

Question
    title = 'aaa'
    content = bbb'
    comments = ???

首先,我想使用嵌入的注释(我认为embed在MongoDB中建议这样做),如下所示:

Question
    title = 'aaa'
    content = 'bbb'
    comments = [ { content = 'xxx', createdAt = 'yyy'}, 
                 { content = 'xxx', createdAt = 'yyy'}, 
                 { content = 'xxx', createdAt = 'yyy'} ]

显而易见,但是我担心这种情况:如果要编辑指定的注释,如何获取其内容和问题?没有_id让我找到一个问题,也没有question_ref让我找到它的问题。(我真是个新手,如果不使用_idand 我不知道有没有办法做到这一点question_ref。)

我必须ref不用embed吗?然后,我必须创建一个新的评论集吗?


无论是否创建字段,所有Mongo对象都使用_ID创建。因此,从技术上讲,每个评论仍将具有ID。
罗比·吉尔福伊

25
@RobbieGuilfoyle不正确–参见stackoverflow.com/a/11263912/347455
pennstatephil 2014年

13
我站得住脚了,谢谢@pennstatephil :)
Robbie Guilfoyle

4
他可能的意思是,所有使用此框架的人都使用_id创建了猫鼬对象–请参见猫鼬子文档
Luca Steeb,2016年

1
学习Mongo数据库关系的一本很好的书是“ MongoDB应用设计模式-O'Reilly”。第一章,谈论这个决定,要嵌入还是参考?
费利佩·托莱多

Answers:


769

这更多的是艺术而不是科学。关于模式Mongo文档是很好的参考,但是这里有一些需要考虑的事项:

  • 尽可能多地投入

    Document数据库的乐趣在于它消除了许多Join。您的第一个本能应该是将尽可能多的内容放置在单个文档中。由于MongoDB文档具有结构,并且由于您可以在该结构中进行有效查询(这意味着您可以获取所需文档的一部分,因此文档大小不必担心太多),因此无需立即标准化数据您将使用SQL。特别是,除其父文档之外没有用的任何数据都应属于同一文档。

  • 可以从多个位置引用的数据分离到其自己的集合中。

    这不是一个“存储空间”问题,而是一个“数据一致性”问题。如果许多记录将引用相同的数据,则更新单个记录并将引用保留在其他位置会更高效且更不会出错。

  • 文件大小注意事项

    MongoDB在单个文档上限制了4MB(16MB,1.8)的大小限制。在GB的数据世界中,这听起来很小,但它还是3万条推文或250个典型的堆栈溢出答案或20张闪烁的照片。另一方面,这比一次要在典型网页上呈现的信息要多得多。首先考虑什么会使您的查询更容易。在许多情况下,对文档大小的关注会过早优化。

  • 复杂的数据结构:

    MongoDB可以存储任意深度嵌套的数据结构,但不能有效地搜索它们。如果数据形成树,林或图,则实际上需要将每个节点及其边缘存储在单独的文档中。(请注意,还有一些专门针对此类数据设计的数据存储)

    指出,不可能返回文档中元素的子集。如果您需要挑选每个文档的一些位,则将它们分开会更容易。

  • 数据一致性

    MongoDB在效率和一致性之间进行权衡。规则是对单个文档的更改始终是原子的,而对多个文档的更新则永远不应被认为是原子的。也没有办法“锁定”服务器上的记录(您可以使用“锁定”字段将其构建到客户端的逻辑中)。设计架构时,请考虑如何保持数据一致。通常,您在文档中保留的越多越好。

对于您要描述的内容,我将嵌入注释,并为每个注释提供一个带ObjectID的ID字段。ObjectID嵌入了一个时间戳,因此您可以根据需要使用它而不是在上创建它。


1
我想添加到OP问题:我的评论模型包含用户名和指向他的头像的链接。考虑到用户可以修改他的姓名/头像,最好的方法是什么?
user1102018

4
关于“复杂数据结构”,似乎可以使用聚合框架返回文档中元素的子集(尝试$ unwind)。
Eyal Roth 2013年

3
错误,这种技术在2012年初在MongoDB中不是不可能的,也不是广为人知。鉴于此问题的普遍性,我鼓励您编写自己的更新答案。恐怕我已经放弃了在MongoDB上进行积极开发的工作,而且我无法在我的原始帖子中向您发表评论。
约翰·米勒

54
16MB = 3000万条推文?每条推文大约有0.5个字节的威胁?
Paolo 2014年

8
是的,看来我差了1000倍,有些人觉得这很重要。我将编辑帖子。每条推文560字节,当我在2011年死记硬面时,twitter仍然与文本消息和Ruby 1.4字符串绑定;换句话说,仍然只有ASCII字符。
John F. Miller


29

如果要编辑指定的注释,如何获取其内容和问题?

您可以按以下子文档进行查询:db.question.find({'comments.content' : 'xxx'})

这将返回整个问题文档。要编辑指定的注释,您必须在客户端上找到注释,进行编辑并将其保存回数据库。

通常,如果您的文档包含对象数组,则会发现这些子对象需要在客户端进行修改。


4
如果两个注释的内容相同,则此方法将无效。有人可能会争辩说我们也可以将作者添加到搜索查询中,如果作者用相同的内容发表了两个相同的评论,这仍然行不通
Steel Brain 2015年

@SteelBrain:如果他保留了注释索引,则点符号可能会有所帮助。参见stackoverflow.com/a/33284416/1587329
serv-inc 2015年

13
我不知道这个答案怎么有34个投票,第二个多人评论了整个系统将破坏的同一件事。这是绝对糟糕的设计,永远不要使用。@user的工作方式是这样
user2073973

21

好吧,我有点迟了,但仍然想分享我的架构创建方式。

我有一个可以用一个词描述的所有事物的模式,就像您在经典OOP中所做的那样。

例如

  • 评论
  • 帐户
  • 用户
  • 网志文章
  • ...

每个模式都可以另存为文档或子文档,因此我为每个模式声明了这一点。

文献:

  • 可以作为参考。(例如,用户发表评论->评论对用户的引用是“制造者”)
  • 是您应用程序中的“根”。(例如,博客->有关于博客的页面)

子文件:

  • 只能使用一次/永远不能作为参考。(例如,评论保存在博客文章中)
  • 在您的应用程序中永远都不是“根”。(该评论仅显示在博客文章页面中,但该页面仍与博客文章有关)

20

我独自研究此问题时遇到了这个小型演示文稿。我对它的布局和信息展示都感到惊讶。

http://openmymind.net/Multiple-Collections-Versus-Embedded-Documents

总结:

通常,如果您有很多[子文档]或它们很大,则最好使用单独的集合。

较小和/或较少的文档往往很适合嵌入。


11
多少钱a lot?3?10个?100?什么large啊 1kb?1MB?3个领域?20个领域?什么是smaller/ fewer
Traxo

1
这是一个很好的问题,我没有一个具体的答案。同一张演示文稿中包含一张幻灯片,上面写着“一个文档,包括所有嵌入的文档和数组,不能超过16MB”,因此这可能是您的门槛,或者只是针对您的特定情况提供了合理/舒适的选择。在我当前的项目中,大多数嵌入式文档是用于1:1关系的,或者在嵌入式文档非常简单的情况下是1:许多。
克里斯·布鲁姆

另请参阅@ john-f-miller当前的热门评论,该评论同时也未提供阈值的具体数字,但确实包含一些其他指针,这些指针应有助于指导您做出决定。
克里斯·布鲁姆

16

我知道这已经很老了,但是如果您正在寻找OP的有关如何仅返回指定注释的问题的答案,则可以使用$(query)运算符,如下所示:

db.question.update({'comments.content': 'xxx'}, {'comments.$': true})

4
如果两个注释的内容相同,则此方法将无效。有人可能会争辩说我们也可以将作者添加到搜索查询中,如果作者用相同的内容发表了两个相同的评论,这仍然行不通
Steel Brain

1
@SteelBrain:先生,打得好,打得好。
JakeStrang

12

是的,我们可以使用文档中的引用。就像sql i joins一样填充另一个文档。在mongo db中,它们没有映射到多个关系文档的联接,而是可以使用填充来实现我们的方案。

var mongoose = require('mongoose')
  , Schema = mongoose.Schema

var personSchema = Schema({
  _id     : Number,
  name    : String,
  age     : Number,
  stories : [{ type: Schema.Types.ObjectId, ref: 'Story' }]
});

var storySchema = Schema({
  _creator : { type: Number, ref: 'Person' },
  title    : String,
  fans     : [{ type: Number, ref: 'Person' }]
});

填充是用其他集合中的文档自动替换文档中指定路径的过程。我们可以填充单个文档,多个文档,纯对象,多个纯对象或查询返回的所有对象。让我们看一些例子。

更好的是您可以获得更多信息,请访问:http : //mongoosejs.com/docs/populate.html


5
猫鼬会为每个填充的字段分别发出请求。这与SQL JOINS不同,因为它们是在服务器上执行的。这包括应用程序服务器和mongodb服务器之间的额外流量。同样,在优化时您可能会考虑这一点。但是,您的答案仍然是正确的。
最多

6

实际上,我很好奇为什么没人谈论UML规范。经验法则是,如果有聚合,则应使用引用。但是,如果它是一个组合,则耦合会更强,并且应该使用嵌入式文档。

您将很快理解为什么这是合乎逻辑的。如果对象可以独立于父对象而存在,那么即使父对象不存在,您也要对其进行访问。由于您无法将其嵌入到不存在的父级中,因此必须使其活在它自己的数据结构中。如果存在父对象,只需在父对象中添加对象的引用即可将它们链接在一起。

真的不知道两者之间的区别是什么?这是解释它们的链接: UML中的聚合与组合


为什么是-1?请给,澄清的原因的解释
Bonjour123


1

如果要编辑指定的注释,如何获取其内容和问题?

如果您已跟踪要更改的评论的数量和评论的索引,则可以使用点运算符例如)。

你可以做f.ex.

db.questions.update(
    {
        "title": "aaa"       
    }, 
    { 
        "comments.0.contents": "new text"
    }
)

(作为在问题内编辑评论的另一种方式)

By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.