MongoDB 多对多关联
Posted
技术标签:
【中文标题】MongoDB 多对多关联【英文标题】:MongoDB Many-to-Many Association 【发布时间】:2011-01-21 03:46:18 【问题描述】:您将如何与 MongoDB 进行多对多关联?
例如;假设您有一个用户表和一个角色表。用户有很多角色,角色有很多用户。在 SQL 领域,您将创建一个 UserRoles 表。
Users:
Id
Name
Roles:
Id
Name
UserRoles:
UserId
RoleId
MongoDB 中如何处理同一种关系?
【问题讨论】:
另见this question和this question的答案 【参考方案1】:根据您的查询需求,您可以将所有内容放入用户文档中:
name:"Joe"
,roles:["Admin","User","Engineer"]
要获得所有工程师,请使用:
db.things.find( roles : "Engineer" );
如果您想在单独的文档中维护角色,则可以在角色数组中包含文档的 _id 而不是名称:
name:"Joe"
,roles:["4b5783300334000000000aa9","5783300334000000000aa943","6c6793300334001000000006"]
并设置以下角色:
_id:"6c6793300334001000000006"
,rolename:"Engineer"
【讨论】:
后者会更好,因为我需要获取所有可用角色的列表。唯一不好的部分是我需要设置关联的两端。在做SQL方式的时候,添加一个UserRole会让User知道Role,Role知道User。这种方式意味着我必须在用户上设置角色,在角色上设置用户。不过我想这很好。 仅仅因为一个数据库不支持sql并不意味着引用不是有用的工具 NoSQL != NoReference 看到这个解释:mongodb.org/display/DOCS/Schema+Design 这似乎不是一个好主意。如果你只有六个角色,当然,但是如果你有 20000 个对象可以链接到 20000 个对象(在多对多关系中)呢?甚至 MongoDB 文档也暗示您应该避免使用可变的、巨大的引用数组。 docs.mongodb.org/manual/tutorial/… 显然,对于与许多对象的多对多关系,您希望使用不同的解决方案(例如文档中的出版商/书籍示例)。在这种情况下,它可以正常工作,并且只会在您创建单独的用户角色文档时使事情复杂化。 这适用于大多数系统,因为角色通常是一小部分,我们通常会选择一个用户,然后查看他/她的角色。但是如果角色很大呢?或者如果我要求你给我一个角色==“工程师”的用户列表怎么办?现在,您必须查询您的整个用户集合(访问所有没有角色 Engineer 的用户),例如仅在数百万这样的用户中获得 2 或 3 个可能具有此角色的用户。单独的表或集合要好得多。【参考方案2】:如果员工和公司是entity-object 尝试使用以下架构:
employee
//put your contract to employee
contracts: item1, item2, item3,...
company
//and duplicate it in company
contracts: item1, item2, item3,...
【讨论】:
这将具有读取性能,但更新应该是原子的,需要一些锁或类似的东西,对吧?【参考方案3】:我没有尝试根据我们多年使用 RDBMS 的经验进行建模,而是发现通过针对读取用例进行优化,使用 MongoDB、Redis 和其他 NoSQL 数据存储对文档存储库解决方案进行建模要容易得多,同时考虑周到写用例需要支持的原子写操作。
例如,“角色中的用户”域的用法如下:
-
角色 - 创建、读取、更新、删除、列出用户、添加用户、删除用户、清除所有用户、用户索引或类似的支持“角色中的用户”(容器 + 自己的元数据等操作)。
用户 - 创建、读取、更新、删除(像独立实体一样的 CRUD 操作)
这可以建模为以下文档模板:
User: _id: UniqueId, name: string, roles: string[]
Indexes: unique: [ name ]
Role: _id: UniqueId, name: string, users: string[]
Indexes: unique: [ name ]
为了支持高频使用,例如来自 User 实体的与角色相关的功能,User.Roles 被有意非规范化,存储在 User 以及具有重复存储的 Role.Users 上。
如果它在文本中不是很明显,但这是使用文档存储库时鼓励的思维类型。
我希望这有助于缩小操作读取方面的差距。
对于写入端,鼓励的是根据原子写入进行建模。例如,如果文档结构需要获取锁,更新一个文档,然后更新另一个文档,可能还有更多文档,然后释放锁,很可能模型已经失败。仅仅因为我们可以构建分布式锁并不意味着我们应该使用它们。
对于角色中的用户模型,扩展我们的原子写避免锁定的操作是从角色中添加或删除用户。在任何一种情况下,成功的操作都会导致更新单个用户和单个角色文档。如果发生故障,很容易执行清理。这是在使用文档存储库的情况下大量出现工作单元模式的原因之一。
真正扩展我们的原子写避免锁定的操作是清除角色,这将导致许多用户更新以从 User.roles 数组中删除 Role.name。这种 clear then 操作通常不被鼓励,但如果需要,可以通过对操作进行排序来实现:
-
从 Role.users 获取用户名列表。
迭代步骤 1 中的用户名,从 User.roles 中删除角色名。
清除 Role.users。
对于最有可能在第 2 步中发生的问题,回滚很容易,因为可以使用第 1 步中的同一组用户名来恢复或继续。
【讨论】:
【参考方案4】:我刚刚偶然发现了这个问题,虽然它是一个老问题,但我认为添加给出的答案中未提及的几个可能性会很有用。此外,在过去几年中,事情发生了一些变化,因此值得强调的是,SQL 和 NoSQL 正在相互靠近。
其中一位评论者提出了一种明智的谨慎态度,即“如果数据是关系型的,就使用关系型”。但是,该注释仅在关系世界中才有意义,其中模式始终位于应用程序之前。
关系世界:结构数据 > 编写应用程序来获取它 NOSQL WORLD:设计应用程序 > 相应地构造数据
即使数据是关系型的,NoSQL 仍然是一种选择。比如一对多关系完全没问题,在MongoDB docs中广泛覆盖
2010 年问题的 2015 年解决方案
自从发布此问题以来,已经进行了认真的尝试以使 noSQL 更接近 SQL。由加利福尼亚大学(圣地亚哥)的 Yannis Papakonstantinou 领导的团队一直在研究FORWARD,这是一种 SQL++ 实现,它可能很快成为解决此处发布的问题的持久性问题的解决方案。
在更实际的层面上,Couchbase 4.0 的发布意味着您第一次可以在 NoSQL 中执行原生 JOIN。他们使用自己的 N1QL。这是来自他们的tutorials 的JOIN
的示例:
SELECT usr.personal_details, orders
FROM users_with_orders usr
USE KEYS "Elinor_33313792"
JOIN orders_with_users orders
ON KEYS ARRAY s.order_id FOR s IN usr.shipped_order_history END
N1QL 允许大多数 SQL 操作,包括聚合、过滤等。
不是那么新的混合解决方案
如果 MongoDB 仍然是唯一的选择,那么我想回到我的观点,即应用程序应该优先于数据结构。没有一个答案提到混合嵌入,即大多数查询的数据都嵌入到文档/对象中,并且为少数情况保留了引用。
示例:信息(除了角色名称)可以等待吗?不请求用户不需要的任何东西,是否可以更快地引导应用程序?
如果用户登录并且他/她需要查看他/她所属的所有角色的所有选项,则可能会出现这种情况。但是,用户是“工程师”,很少使用此角色的选项。这意味着应用程序只需要为工程师显示选项,以防她/他想要单击它们。
这可以通过一个文档来实现,该文档在开始时告诉应用程序 (1) 用户属于哪些角色以及 (2) 从哪里获取有关与特定角色相关的事件的信息。
_id: ObjectID(),
roles: [[“Engineer”, “ObjectId()”],
[“Administrator”, “ObjectId()”]]
或者,更好的是,在角色集合中索引 role.name 字段,您可能也不需要嵌入 ObjectID()。
另一个例子:关于所有角色的信息是否一直被请求?
也可能是用户登录仪表板并且 90% 的时间执行与“工程师”角色相关的任务。可以为该特定角色完整地完成混合嵌入,并仅保留其余部分的引用。
_id: ObjectID(),
roles: [name: “Engineer”,
property1: value1,
property2: value2
,
[“Administrator”, “ObjectId()”]
]
无模式不仅仅是 NoSQL 的一个特点,在这种情况下它可能是一个优势。在用户对象的“角色”属性中嵌套不同类型的对象是完全有效的。
【讨论】:
【参考方案5】:有两种方法可以使用:
第一种方法
将参考链接添加到用户文档角色列表(数组)中:
'_id': ObjectId('312xczc324vdfd4353ds4r32')
user:faizanfareed,
roles : [
'roleName':'admin', # remove this because when we will be updating some roles name we also need to be update in each user document. If not then ignore this.
roleId: ObjectID('casd324vfdg65765745435v')
,
'roleName':'engineer',
roleId: ObjectID('casd324vfdvxcv7454rtr35vvvvbre')
,
]
并且(根据查询要求)我们还可以将用户参考ID添加到角色文档用户列表(数组)中:
roleName:admin,
users : [userId: ObjectId('312xczc324vdfd4353ds4r32'), .......]
但是将用户ID添加到角色文档大小将超过16MB,这一点都不好。如果角色文档的大小不超过并且用户的大小是有界的,我们可以使用这种方法。如果不需要,我们可以仅将角色 ID 添加到用户文档中。
传统的第二种方法
创建新集合,其中每个文档都包含用户和角色的 id。
'_id': ObjectId('mnvctcyu8678hjygtuyoe')
userId: ObjectId('312xczc324vdfd4353ds4r32')
roleId: ObjectID('casd324vfdg65765745435v')
不会超出文档大小,但这种方法读取操作并不容易。
根据要求采用第一种或第二种方法。
关于此的最终决定:采用第一种方法,仅将 roleId 添加到用户文档数组中,因为没有任何角色不会大于用户。用户文档大小不会超过 16MB。
【讨论】:
以上是关于MongoDB 多对多关联的主要内容,如果未能解决你的问题,请参考以下文章