文档型数据库中的关系?

18

我对文档导向型数据库非常感兴趣,想要尝试一下 MongoDB。于是我开始了一个相当简单的项目(一个问题追踪器),但是在思考非关系型数据库时遇到了困难。

我的问题:

  1. 我有两个相关联的对象(例如 issue = {code:"asdf-11", title:"asdf", reporter:{username:"qwer", role:"manager"}} - 这里有一个与问题相关的用户)。我应该创建另一个文档'用户'并在'问题'文档中引用它的ID(像关系型数据库一样),还是应该将所有用户数据留在子文档中?

  2. 如果我在一个文档中有多个对象(子文档),我能否在单个查询中更新它们全部?


请查看以下链接:http://ayende.com/blog/4465/that-no-sql-thing-the-relational-modeling-anti-pattern-in-document-databases - Łukasz Adamus
这是一个有趣的视频,关于这个话题:http://www.10gen.com/presentations/schema-design-basics-1 - Abby
MongoDB本身是一个非常出色的NoSQL文档导向数据库。当您将其与Mongoose ORM配对使用时,它真正变得生动起来。我只是盲目地假设您正在使用Node.js,如果不是,我很抱歉。 MongoDB和Mongoose一起使NoSQL能够实现在RDBMS中可以完成的任务,具有20倍的灵活性和功能。 Mongoose提供了“population”的概念,这为您提供了所需的文档之间的连接。再次强调,Mongoose是javascript / Node.js,但我相信其他语言也有其他ORM。 - regretoverflow
请查看@oyatek对此问题的回答:http://stackoverflow.com/questions/13325346/embedded-or-referenced-relations/13408664#13408664 - mattexx
@mattexx 不是针对你,但我不认为那是一个“实质性”的答案,而且也不总是正确的。 - Sammaye
@Sammaye 不会有任何冒犯,只是想指出类似的问题。 - mattexx
6个回答

4
我完全不了解面向文档的数据库,现在我正在尝试使用node.js和mongodb开发一种CMS,所以我面临着与您相同的问题。
通过试错,我找到了这个经验法则:为每个可能成为查询“主题”的实体创建一个集合,同时将其余部分嵌入其他对象中。
例如,在博客条目中的评论可以被嵌入,因为它们通常与条目本身绑定在一起,我无法考虑到可以在所有评论上进行有用的全局查询。另一方面,附加到帖子的标签可能值得拥有自己的集合,因为即使它们与帖子绑定,您也可能希望全局考虑所有标签(例如制作趋势主题列表)。

“我为可能成为我的查询“主题”的每个实体创建一个集合,同时将其余部分嵌入其他对象中。” 这句话的意思是什么? - Sammaye
还有,为什么你使用了博客关系,这是一种与本问题所解释的不同类型的子文档? - Sammaye

2
在我看来,这其实很简单。嵌入式文档只能通过它们的主文档访问。如果你可以想象到需要在主文档之外查询对象,那么就不要将其嵌入。使用引用即可。
针对您的示例:
issue = {code:"asdf-11", title:"asdf", reporter:{username:"qwer", role:"manager"}}

我会将问题和报告者分别放在不同的文档中,并在问题中引用报告者。您还可以在报告者中引用问题列表。这样,您就不会在问题中重复报告者,可以分别查询它们,可以通过问题查询报告者,也可以通过报告者查询问题。如果您将报告者嵌入问题中,那么只能单向查询,即通过问题查询报告者。
如果您嵌入文档,则可以在单个查询中更新所有文档,但必须在每个主文档中重复更新。这是使用参考文档的另一个好理由。

1
嵌入式文档可以在不需要主文档的情况下检索,例如:要计算所有报告人为“qwer”的报告者数量:db.tickets.aggregate({$match:{"reporter.username":"qwer"}},{$group:{_id:'$reporter.username', c:{$sum:1}}}) 另外:“如果您嵌入文档,则无法在单个查询中更新所有文档。”是的,您可以进行原地更新;问题在于如果用户重复,更新将在多个主文档之间重复,但即使如此,它也是一个原地$set更新,您不必加载每个主文档。 - Sammaye
@Sammaye 感谢您的澄清,我已经更新了我的答案。很抱歉我有些困惑,我主要是通过ORM层来使用mongodb的,所以对其基础知识的理解有些偏差。 - mattexx
实际上,许多ORM都是围绕着平面结构而设计的,对于子文档的处理能力非常差甚至没有。 - Sammaye

1

MongoDB和其他“NoSQL”产品的美妙之处在于没有需要设计的模式。我使用MongoDB,我喜欢它,因为不需要编写SQL查询和糟糕的JOIN查询!所以来回答你的两个问题。

1 - 如果您创建多个文档,则需要对数据库进行两次调用。并不是说这是一件坏事,但如果您可以将所有内容放入一个文档中,为什么不这样做呢?我记得以前使用MySQL时,我会创建一个“博客”表和一个“评论”表。现在,我将评论附加到同一集合(也称为表)中的记录中,并继续构建它。

2 - 是的...


当然,在博客对象中保留注释是有意义的,但规范化/DRY仍然适用:将相同的数据放在多个位置是不好的形式,因此,如果您想让用户在整个集合中出现多次,最好通过ID引用它们并将它们放在自己的集合中。美妙之处在于,您不必创建特殊的集合来表示多对多关系。 - Alan Plum
1
非常好的建议luckytaxi。需要注意的一件事是32位系统的最大文档大小为2mb,64位系统的最大文档大小为4mb。只要您确定文档永远不会达到此大小,请去做。对于松散相关的数据,例如用户和错误,我建议将它们保持在两个单独的集合中。如果您真的想这样做,您也可以在Mongo中进行某种“联接”,尽管我以前从未走过这条路。 - Joshua Burns
在MongoDB中,你确实需要在关系模式中执行JOIN操作,但是这只是在客户端进行的,它不支持服务器端的JOIN操作。 - Sammaye
实际上,设计总是有一个“模式”,而这个模式就在你的代码中。你可以使用一些ORM框架来处理它,完全不需要使用SQL。 - Konrad

0
重新回答这个问题,因为原始答案由于阅读错误而将关系颠倒了。
问题 = {code:"asdf-11", title:"asdf", reporter:{username:"qwer", role:"manager"}}
关于是否嵌入一些关于工单创建者的重要信息是一个明智的决定还是不明智的决定,取决于系统的具体情况。
如果您让这些用户能够登录并报告他们发现的问题,那么您可能希望将该关系拆分到一个用户集合中。
另一方面,如果不是这种情况,那么您可以轻松地使用这个模式。我在这里看到的一个问题是,如果您希望联系报告人,而他们的工作角色发生了变化,那就有点尴尬;然而,这是一个现实世界的困境,而不是数据库的困境。
由于子文档表示与报告人之间的一对一关系,您也不应该遇到我原来答案中提到的碎片化问题。
这个模式存在一个明显的问题,即重复数据的重复更改(规范化形式的问题)。
让我们举个例子。想象一下,你遇到了我之前提到的现实世界的困境,一个名叫Nigel的用户希望他的角色从现在开始反映他的新工作职位。这意味着你必须更新所有Nigel是报告人的行,并将他的角色更改为那个新职位。对于MongoDB来说,这可能是一个耗时和资源消耗大的查询。
再次自相矛盾,如果每个用户只有大约100个票(即可管理的内容),那么更新操作可能不会太糟糕,并且实际上对于数据库来说很容易管理;此外,由于文档(希望如此)缺乏移动,因此这将是完全就地更新。
因此,无论是否应该嵌入,都严重取决于您的查询和文档等,但是,我认为这种模式不是一个好主意;具体原因是在许多根文档中更改数据的重复。从技术上讲,是的,你可以做到,但我不会尝试。
我会把这两个分开。
引用:
如果我的文档中有对象(子文档),我可以在单个查询中更新它们吗?
就像我原始答案中的关系样式一样,是的,而且很容易。
例如,让我们更新 Nigel 的角色为 MD(如之前所示),并将工单状态更改为已完成:
db.tickets.update({'reporter.username':'Nigel'},{$set:{'reporter.role':'MD', status: 'completed'}})

因此,在这种情况下,单个文档模式确实使CRUD更容易。

需要注意的一件事是,根据您的英语,您不能使用位置运算符更新根文档下的所有子文档。相反,它只会更新找到的第一个子文档。

再次希望这有意义,我没有漏掉任何内容。祝好运!


原始答案

这里我有一个与问题相关的用户。我应该创建另一个名为“用户”的文档,并通过其ID在“问题”文档中引用它(就像在关系数据库中一样),还是应该将所有用户数据放在子文档中?

这是一个值得考虑的问题,在继续之前需要一些背景知识。

首先要考虑的是问题的大小:

issue = {code:"asdf-11", title:"asdf", reporter:{username:"qwer", role:"manager"}}

这个文本并不是很大,而且由于您不再需要reporter信息(该信息将在根文档中),因此它可能会更小,但是问题从来都不是那么简单。例如,如果您查看MongoDB JIRA:https://jira.mongodb.org/browse/SERVER-9548(作为证明我的观点的随机页面),一个“票”的内容实际上可能相当可观。

唯一能够从嵌入式票据中获得真正好处的方法是,如果您可以将所有用户信息存储在单个16 MB的连续存储块中,这是BSON文档的最大大小(目前由mongod强制执行)。

我认为您无法将所有票据存储在单个用户下。

即使您将票据缩小到一个代码、标题和描述,您仍然可能遭受由于 MongoDB 中文档的定期更新和更改所导致的“瑞士奶酪”问题,就像这个:http://www.10gen.com/presentations/storage-engine-internals 是我所说的内容的好参考。
通常情况下,用户将多个票据添加到其根用户文档中,您会看到这个问题。这些票据本身也会发生变化,但可能不会以剧烈或频繁的方式发生变化。
当然,您可以通过使用2的幂次分配大小的能力http://docs.mongodb.org/manual/reference/command/collMod/#usePowerOf2Sizes来在一定程度上解决这个问题。
好吧,假设您只有codetitle,那么是的,您可以将票据作为子文档存储在根用户中而不会遇到太多问题,但是,这取决于赏金任务的具体要求,目前还未提及。
如果我在一个文档中有对象(子文档),我可以使用单个查询更新它们吗?
是的,非常容易。这是嵌套时变得更容易的一件事情。你可以使用以下查询:
db.users.update({user_id:uid,'tickets.code':'asdf-1'}, {$set:{'tickets.$.title':'Oh NOES'}})

需要注意的是,您只能使用位置操作符一次更新一个子文档。因此,这意味着您不能在单个原子操作中将单个用户的所有票据日期更新为未来5天。

至于添加新票据,那很简单:

db.users.update({user_id:uid},{$push:{tickets:{code:asdf-1,title:"Whoop"}}})

是的,根据您的查询,您可以非常简单地在单个调用中更新整个用户数据。

这是一个相当长的答案,希望我没有漏掉任何东西,希望能帮到您。


没有人提议将票证嵌入到用户文档中,这样做是愚蠢的。我不明白为什么你要讨论是否这是个好主意。问题是关于是否将用户信息(对于报告它的用户?)嵌入到每个票证中而不仅仅是一个引用。 - Asya Kamsky
@AsyaKamsky 嗯,重新阅读问题后发现我把关系搞反了。 - Sammaye
@AsyaKamsky 进行了编辑,我有点累,但我认为我可能已经涵盖了它。 - Sammaye

0

在面向文档的数据库中进行模式设计一开始可能会感到困难,但是我使用Symfony2和MongoDB构建我的创业公司时发现,80%的时间就像关系型数据库一样。


首先,将其视为普通的数据库:

要开始,只需像使用关系型数据库一样创建模式:

每个实体都应该有自己的集合特别是如果您需要对其中的文档进行分页

(在Mongo中,您可以在某种程度上对嵌套的文档数组进行分页,但功能有限)


然后只需删除过于复杂的规范化:

  • 我需要一个单独的类别表吗?(只需将类别作为字符串或嵌入式文档写入列/属性中)
  • 我可以直接在作者集合中存储评论计数吗?(然后使用事件更新计数,例如在Doctrine ODM中)

嵌入式文档:

只有在以下情况下使用嵌入式文档:

  • 为了清晰明了(例如:在用户集合中嵌套文档,如addressInfobillingInfo
  • 用于存储标签/分类(例如:[ name: "Sport", parent: "Hobby", page: "/sport" ]
  • 用于存储简单的多个值(例如,在用户集合中存储专业列表、个人网站列表)

不要在以下情况下使用嵌入式文档:

  • 父文档将变得过大
  • 需要对其进行分页
  • 感觉实体足够重要,应该拥有自己的集合

在集合中重复值并预先计算计数:

如果您需要在 where 条件中查询每个值,则从一个集合复制一些列/属性值到另一个集合中。 (请记住,没有 join

例如:在 Ticket 集合中还要放置作者姓名(不仅是 ID

此外,如果您需要计数器(用户打开的票数,按类别分类等),请预先计算它们。


嵌入式引用:

当您拥有一个一对多或多对多的引用时,请使用带有引用文档ID列表的嵌入式数组(请参见 MongoDB DB Ref)。

如果引用文档被删除,您需要再次使用事件来删除ID。 (如果您使用Doctrine ODM,则有一个扩展程序: 引用完整性

这种引用由Doctrine ODM直接管理: 多个引用


修复错误很容易:

如果您在模式设计中发现错误,只需几行JavaScript代码直接在Mongo控制台中运行即可轻松解决。

(存储过程变得简单:无需复杂的迁移脚本)

警告:不要使用Doctrine ODM Migrations,否则您将会后悔。


分页子文档非常容易,使用聚合框架还可以进行复杂的排序等操作。此外,MongoDBRef是一种特殊类型的引用,大多数情况下仅使用OjbectId会占用更少的空间。 - Sammaye
分页是通过使用$slice修饰符实现的,但问题在于您不能使用与分页集合相同的查询命令。聚合框架可以帮助解决问题,但请查看有关数组的其他问题:https://jira.mongodb.org/secure/QuickSearch.jspa 主要问题是:它们不是集合。 - Madarco
对于MongoDBRef:在Doctrine ODM中,您可以使用simple=true修饰符将引用存储为仅ObjectId。但是,DbRef更干净,因为它还存储了所引用文档的集合和数据库。此外,如果您没有数百万行数据,为什么要费心呢? - Madarco
1
请注意,引用某个ORM的行为可能不是一个好主意,在这个问题中没有提到任何特定的语言,因此您不能在该特定程序之外依赖该功能。DBref使用了更多的空间和工作集,特别是如果您正在处理工作集中的700万行(如果),您很快就会注意到到处使用DBRef可能导致需要更改模式。 - Sammaye
1
子文档存在问题,然而,大多数这些问题可以通过特定的投影和模式定向机制来减轻(例如,在根文档中存储100个元素的批次,并仅投影所需内容)。但是确实存在使用子文档会增加工作集大小以及使用$push等内存操作符可能导致性能问题的风险。 - Sammaye
我还应该指出,MongoDB没有存储过程的概念,迁移脚本也不同于存储过程,它们是完全不同的用于完全不同任务的东西。 - Sammaye

0

我喜欢MongoDB,但我必须说,在我的下一个项目中,我会更加谨慎地使用它。

具体来说,我没有像人们承诺的那样在嵌入式文档功能方面取得太多成功。

嵌入式文档似乎对组合很有用(请参见UML组合),但不适用于聚合。叶节点很棒,但是你的对象图中间的任何内容都不应该是嵌入式文档。这将使搜索和验证数据变得更加困难。

MongoDB中绝对更好的一件事是你的多对X关系。你只需使用两个表就可以实现多对多,并且可以在任一表上表示多对一关系。也就是说,你可以在N行中放置1个键,或者在1行中放置N个键,或者两者兼而有之。值得注意的是,执行集合操作(交集、并集、不相交集等)的查询实际上可以被你的同事理解。我从来没有对SQL中的这些查询感到满意。我经常只能安慰自己“其他两个人会理解这个”。

如果你的数据变得非常大,你就会知道插入和更新可能会受到索引成本的限制。在MongoDB中,你需要更少的索引;A-B-C上的一个索引可以用于查询A、A&B或A&B&C(但不能查询B、C、B&C或A&C)。此外,反转关系的能力使你可以将一些索引移动到辅助表中。我的数据还没有变得足够大,但我希望这样做会有所帮助。


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接