MongoDB 模式设计模式(一)

2025-06-11

MongoDB 模式设计模式(一)

MongoDB 已成为最流行的 NoSQL 数据库之一。由于它易于融入 JavaScript 生态系统,因此经常被用作 MEAN/MERN 技术栈的一部分。
互联网上有数百个教程、大量课程和一些书籍,教你如何成为一名使用 MongoDB 作为技术栈数据库系统(MERN/MEAN 中的 M)的全栈开发者。
问题在于,它们中的大多数都没有关注 MongoDB 的模式设计模式。因此,基于设计好的模式进行的操作/查询性能很差,并且/或者无法扩展。

设计 MongoDB 模式时必须面对的主要问题之一是如何模拟“一对多”关系。

许多初学者认为,在 MongoDB 中实现“一对多”关系的唯一方法是将子文档数组嵌入到父文档中,但事实并非如此。可以嵌入文档并不意味着应该嵌入文档。事实上,无限增长的数组会降低性能。此外,文档大小上限为 16MB。

在设计 MongoDB 模式时,你必须首先思考这个问题:关系的基数是多少?“一对几”“一对多”还是“一对亿”?根据具体情况,你可以使用不同的格式来建模关系。

一对多

举个“一对多”的例子,比如某个人的地址。这是一个很好的嵌入用例——你可以把地址放在 Person 对象内部的一个数组里:

> db.person.findOne()
{
  name: 'Manuel Romero',
  ssn: '123-456-7890',
  addresses : [
     { street: '123 Sesame St', city: 'Anytown', cc: 'USA' },
     { street: '123 Avenue Q', city: 'New York', cc: 'USA' }
  ]
}
Enter fullscreen mode Exit fullscreen mode

优点:

  • 主要优点是您不必执行单独的查询来获取嵌入的详细信息。

缺点:

  • 主要缺点是您无法将嵌入的详细信息作为独立实体进行访问。

一对多

“一对多”关系的一个例子可能是替换零件订购系统中某个产品的零件。每个产品最多可能有几百个替换零件,但不会超过几千个。(所有这些不同尺寸的螺栓、垫圈和密封垫加起来就很庞大了。)这是一个很好的引用用例——您可以将零件的 ObjectID 放在产品文档中的一个数组中。

部分文件:

> db.parts.findOne()
{
    _id : ObjectID('AAAA'),
    partno : '123-aff-456',
    name : '#4 grommet',
    qty: 94,
    cost: 0.94,
    price: 3.99
}
Enter fullscreen mode Exit fullscreen mode

产品文档:

> db.products.findOne()
{
    name : 'left-handed smoke shifter',
    manufacturer : 'Acme Corp',
    catalog_number: 1234,
    parts : [     // array of references to Part documents
        ObjectID('AAAA...'),    // reference to the #4 grommet above
        ObjectID('F17C...'),    // reference to a different Part
        ObjectID('D2AA...'),
        // etc
    ]
Enter fullscreen mode Exit fullscreen mode

优点:

  • 每个部分都是一个独立的文档,因此很容易搜索和独立更新它们。

  • 此模式允许您让多个产品使用单独的部件,因此您的一对多模式就变成了多对多模式,而无需任何连接表!

缺点:

  • 必须执行第二次查询才能获取有关产品零件的详细信息。

一对多与非规范化

想象一下,我们的 Products 集合中有一个频繁的操作:给定一个部件的名称,查询该产品是否存在该部件。按照我们实现的方法,我们需要执行两个查询。一个用于获取产品所有部件的 ObjectID,另一个用于获取部件的名称。但是,如果这是我们应用程序的常见数据访问模式,我们可以将部件的字段名称非规范化,放入产品部件数组中:

> db.products.findOne()
{
    name : 'left-handed smoke shifter',
    manufacturer : 'Acme Corp',
    catalog_number: 1234,
    parts : [
        {
         ObjectID('AAAA...'),
         name: '#4 grommet'
        },
        {
         ObjectID('F17C...'),    
         name: '#5 another part name'
        },
        {
         ObjectID('D2AA...'),
         name: '#3 another part name 2'
        }
        // etc
    ]
Enter fullscreen mode Exit fullscreen mode

优点:

  • 我们可以通过一个查询查看属于某个产品(其名称)的所有部件。

缺点:

  • 当非规范化字段(本例中为name字段)很少更新时,非规范化是有意义的。如果我们对一个经常更新的字段进行非规范化,那么查找和更新所有实例的额外工作可能会抵消我们通过非规范化所节省的成本。由于零件的名称很少更改,所以对我们来说,这样做是可以的。

一到数千万亿

一个“一到亿亿”的例子可能是收集不同机器日志消息的事件日志系统。任何给定的主机都可能生成足够的消息,足以溢出 16 MB 的文档大小,即使数组中存储的只是 ObjectID。这是“父引用”的经典用例——你有一个主机的文档,然后将主机的 ObjectID 存储在日志消息的文档中。

宿主文档:

> db.hosts.findOne()
{
    _id : ObjectID('AAA2...'),
    name : 'goofy.example.com',
    ipaddr : '127.66.66.66'
}
Enter fullscreen mode Exit fullscreen mode

消息文档:

>db.logmsg.findOne()
{
    time : ISODate("2014-03-28T09:42:41.382Z"),
    message : 'cpu is on fire!',
    host: ObjectID('AAA2...')       // Reference to the Host document
}
Enter fullscreen mode Exit fullscreen mode

结论

根据我们的一对多关系的基数,我们可以选择三种基本的一对多模式设计之一:

  1. 如果基数是一对多,并且不需要在父对象上下文之外访问嵌入对象,则嵌入 N 侧。

  2. 如果基数是一对多,或者 N 侧对象由于某种原因应该独立存在,则使用对 N 侧对象的引用数组。

  3. 如果基数是一到数千万亿,则使用对 N 侧对象中的一侧的引用。

请记住:我们如何建模数据完全取决于我们特定应用程序的数据访问模式。我们希望构建数据以匹配应用程序查询和更新数据的方式。

参考

鏂囩珷鏉ユ簮锛�https://dev.to/mrm8488/mongodb-schema-design-patterns-i-4gdp
PREV
如何从头开始为你的项目配置 ESLint
NEXT
Asp Net Core - 使用 JWT 进行 Rest API 授权(角色、声明和策略)- 一步步