MongoDB 多对多协会

如何与 MongoDB 进行多对多关联?

例如,假设您有一个 Users 表和一个 Roles 表。用户有许多角色,而角色有许多用户。在 SQL 领域,您将创建一个 UserRoles 表。

Users:
Id
Name


Roles:
Id
Name


UserRoles:
UserId
RoleId

MongoDB 中如何处理相同类型的关系?

96215 次浏览

根据您的查询需要,您可以将所有内容放在用户文档中:

{name:"Joe"
,roles:["Admin","User","Engineer"]
}

为了得到所有的工程师,使用:

db.things.find( { roles : "Engineer" } );

如果希望在单独的文档中维护角色,那么可以在角色数组中包含文档的 _ id,而不是名称:

{name:"Joe"
,roles:["4b5783300334000000000aa9","5783300334000000000aa943","6c6793300334001000000006"]
}

然后设定角色,比如:

{_id:"6c6793300334001000000006"
,rolename:"Engineer"
}

以防员工和公司是实体对象 尝试使用以下模式:

employee{
//put your contract to employee
contracts:{ item1, item2, item3,...}
}


company{
//and duplicate it in company
contracts:{ item1, item2, item3,...}
}

与其试图根据我们多年使用 RDBMS 的经验建模,我发现使用 MongoDB、 Redis 和其他 NoSQL 数据存储更容易建模文档存储库解决方案,方法是优化读用例,同时考虑写用例需要支持的原子写操作。

例如,“角色中的用户”域的使用如下:

  1. 角色-创建,阅读,更新,删除,列出用户,添加用户,删除用户,清除所有用户,用户索引或类似的支持“是用户在角色”(操作像一个容器 + 自己的元数据)。
  2. User-Create,Read,Update,Delete (CRUD 操作就像一个独立的实体)

这可以建模为以下文档模板:

User: { _id: UniqueId, name: string, roles: string[] }
Indexes: unique: [ name ]
Role: { _id: UniqueId, name: string, users: string[] }
Indexes: unique: [ name ]

为了支持高频使用,例如来自 User 实体 User 的与角色相关的特性。角色被有意地反规范化,存储在用户和角色上。具有重复存储的用户。

如果在文本中不容易看到,但是在使用文档存储库时鼓励使用这种思维方式。

我希望这有助于弥补操作读方面的差距。

对于写方,鼓励根据原子写建模。例如,如果文档结构需要获取一个锁,更新一个文档,然后更新另一个文档,可能还有更多文档,然后释放锁,那么模型可能已经失败。仅仅因为我们可以构建分布式锁并不意味着我们应该使用它们。

对于“角色中的用户”模型,扩展锁的原子写回避的操作是从“角色”中添加或删除“用户”。在任何一种情况下,成功的操作都会导致更新单个 User 和单个 Role 文档。如果某些事情失败了,则很容易执行清理。这就是为什么在使用文档存储库的地方经常出现工作单元模式的原因之一。

真正延伸我们对锁的原子写回避的操作是清除一个 Role,这将导致许多 User 更新以从 User.Role 数组中删除 Role.name。Clear then 的这种操作通常不受鼓励,但如果需要,可以通过下令执行操作:

  1. 从 Role.users 获取用户名列表。
  2. 重复步骤1中的用户名,从 User.role 中删除角色名。
  3. 清除 Role.users。

对于最有可能发生在步骤2中的问题,回滚很容易,因为可以使用步骤1中的相同用户名集来恢复或继续。

我只是偶然发现了这个问题,虽然这是一个老问题,但我认为增加一些在给出的答案中没有提到的可能性是有用的。另外,在过去的几年里,事情有了一些进展,所以值得强调的是 SQL 和 NoSQL 正在相互靠近。

其中一位评论者提出了明智的警告态度,即“如果数据是关系型的,那么就使用关系型的”。但是,这种注释只在关系世界中有意义,因为模式总是出现在应用程序之前。

关系世界: 结构数据 > 编写应用程序获取数据
NOSQL WORLD: < em > 设计应用程序 > 相应地结构化数据

即使数据是关系型的,NoSQL 仍然是一个选项。例如,一对多关系根本不是问题,而且在 MongoDB 文档中广泛涉及

2010年问题的2015年解决方案

自从发布这个问题以来,已经有很多努力使 noSQL 更接近 SQL。加利福尼亚大学(圣地亚哥)的 Yannis Papakonstantinou 领导的团队一直在研究 前进,这是一个 SQL + + 的实现,可能很快就会成为解决持续存在的问题的方法,就像上面提到的那样。

在更实际的层面上,Couchbase 4.0的发布意味着,您第一次可以在 NoSQL 中执行本地 JOIN。他们使用自己的 N1QL。这是 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 集合中索引 role.name 字段,而且您可能也不需要嵌入 ObjectID ()。

另一个例子: 关于所有请求的角色的信息是否一直存在?

也可能是用户登录到仪表板,90% 的时间执行与“工程师”角色相关的任务。混合嵌入可以完全针对该特定角色完成,并且只为其他角色保留引用。

{_id: ObjectID(),
roles: [{name: “Engineer”,
property1: value1,
property2: value2
},
[“Administrator”, “ObjectId()”]
]
}

无模式不仅仅是 NoSQL 的一个特征,在这种情况下它可能是一个优势。在用户对象的“ Roles”属性中嵌套不同类型的对象是完全有效的。

有两种方法可以使用:

第一次进场

在用户文档角色列表(数组)中添加引用链接:

{
'_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,因为 no of role 不会大于-user。用户文档大小不会超过16MB。