繁体   English   中英

MongoDB 多对多关联

[英]MongoDB Many-to-Many Association

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

例如; 假设您有一个用户表和一个角色表。 用户有很多角色,角色有很多用户。 在 SQL 域中,您将创建一个 UserRoles 表。

Users:
    Id
    Name

Roles:
    Id
    Name

UserRoles:
    UserId
    RoleId

MongoDB 中如何处理同类关系?

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

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

要获得所有工程师,请使用:

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

如果要在单独的文档中维护角色,则可以在角色数组中包含文档的 _id 而不是名称:

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

并设置如下角色:

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

我没有尝试根据我们多年来使用 RDBMS 的经验进行建模,而是发现通过优化读取用例,同时考虑原子性,使用 MongoDB、Redis 和其他 NoSQL 数据存储对文档存储库解决方案进行建模要容易得多写用例需要支持的写操作。

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

  1. 角色 - 创建、读取、更新、删除、列出用户、添加用户、删除用户、清除所有用户、用户索引或类似功能以支持“用户是否在角色中”(类似于容器 + 自己的元数据的操作)。
  2. 用户 - 创建、读取、更新、删除(像独立实体一样的 CRUD 操作)

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

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

为了支持高频使用,例如来自 User 实体的与 Role 相关的功能,User.Roles 被故意非规范化,存储在 User 以及具有重复存储的 Role.User 上。

如果它在文本中不是很明显,但这是使用文档存储库时鼓励的思维类型。

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

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

对于角色模型中的用户的情况,扩展我们对锁的原子写避免的操作是从角色中添加或删除用户。 在任何一种情况下,成功的操作都会导致更新单个用户和单个角色文档。 如果出现故障,很容易执行清理。 这是在使用文档存储库的地方经常出现工作单元模式的原因之一。

真正扩展我们对锁的原子写避免的操作是清除 Role,这将导致许多 User 更新以从 User.roles 数组中删除 Role.name。 这种 clear then 操作通常是不鼓励的,但如果需要,可以通过对操作进行排序来实现:

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

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

我刚刚偶然发现了这个问题,虽然它是一个旧问题,但我认为添加一些未在给出的答案中提到的可能性会很有用。 此外,过去几年事情发生了一些变化,因此值得强调的是 SQL 和 NoSQL 正在相互接近。

其中一位评论者提出了“如果数据是关系型的,就使用关系型”这一明智的谨慎态度。 但是,该评论仅在关系世界中有意义,其中模式始终位于应用程序之前。

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

即使数据是关系数据,NoSQL 仍然是一种选择。 例如,一对多关系完全没有问题,并且在MongoDB 文档中有广泛的介绍

2010 年问题的 2015 年解决方案

自从这个问题被发布以来,已经有认真的尝试让 noSQL 更接近 SQL。 由加利福尼亚大学(圣地亚哥)的 Yannis Papakonstantinou 领导的团队一直致力于FORWARD ,这是 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.name 字段,您可能也不需要嵌入 ObjectID()。

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

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

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

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

如果员工和公司是实体对象,请尝试使用以下模式:

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

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

有两种方法可以使用:

第一种方法

将参考链接添加到用户文档角色列表(数组)中:

{
  '_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。

暂无
暂无

声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM