mongodb设计模式

No Schema –> Freedom Schema
数据库模式(Database Schema)是概念层数据模型在物理层描述的集合,
注意是数据模型在物理层的描述,而不是实现,实现是数据库引擎的工作。
在关系型数据库中模式也就是:
声明a表有哪些字段,b表有哪些字段,a表和b表是如何关联在一起的。
开始接触mongodb时也一直以为mongodb是无模式设计(no schema),
譬如我们在创建表时无需提前声明字段类型,直接使用即可。
但这只是mongodb模式设计的表象:没有模式就是最好的模式。
当我们要实现类似mysql中的多表join操作时,
这时你会发现mongodb模式设计的本质:自由模式才是最好的模式。
最近在简书上看到一篇关于Mongodb模式设计的文章
翻译略显生硬,理解起来有些吃力,看了原文后发现中文版省略了很多关键细节和代码,
所以我重新翻译了下,希望对感兴趣的同学有所帮助。
这里仍然要感谢译者,有些地方翻译得还是很精妙的。


By William Zola, Lead Technical Support Engineer at MongoDB
我在MongoDB工作时经常遇到很多用户向我咨询这个问题:
“我在关系型数据库方面很有经验,但在非关系型数据库方面刚入门。
在mongodb中我如何针对一对多关系进行建模呢?”
这个问题不是三言两语就能说得清的,因为这个问题的解决方式有很多种。
MongoDB把这种一对多关系简化成了一对N,接下来我会教你如何针对一对N关系进行建模。
这个话题有许多内容需要讨论,所以我会分三部分进行说明。
在第一部分,我会讨论针对一对N关系建模的三种基础方案;
在第二部分,我将会覆盖更多高级内容,包括反范式化和双向引用;
在第三部分,我将会回顾各种方案,并就如何从上千种方案中挑选适合的方案给出建议。
很多初学者认为在MongoDB中针对一对N建模唯一的方案:
就是在父文档中内嵌一个子文档。
但是这是不准确的,因为你可以这么做,但不代表你必须这么做。
当你设计一个MongoDB模式时,你需要先问自己一个在使用关系型数据库时不会考虑的问题:
这种一对N关系中的N到底是什么样的规模?
你需要意识到自己的场景是一对少,一对多,还是一对很多。
不同的场景下你的建模也将不同。

初级: 一对少

针对个人需要保存少数几个地址进行建模的场景下使用内嵌文档是最合适不过了,
你可以在person文档中直接嵌入addresses数组。

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

这种设计具有内嵌文档设计中所有的优缺点,
它的优点是:你不需要单独执行一条语句去获取内嵌的内容;
它的缺点是:你无法把这些内嵌文档当做单独的实体去访问。
例如:假如你在设计一个追踪系统,每个用户将会被分配若干任务,
假如像上面的代码一样内嵌这些任务到用户文档,
你将会发现,查询截止到明天的所有任务将会变得非常困难,
我会在后面针对这个用例提供一些适当的设计。

初级: 一对多

以产品零件订货系统为例,每个商品有数百个可替换的零件,但是不会超过数千个。
这个用例很适合使用引用—将零件的objectid作为数组存放在商品文档中,
(在这个例子中的ObjectId我使用更加易读的2字节,现实世界中他们可能是由12个字节组成的)。

> db.parts.findOne() //每个零件都有它自己的文档
{
_id : ObjectId('AAAA'),
partno : '123-aff-456',
name : '#4 grommet',
qty: 94,
cost: 0.94,
price: 3.99
}
> db.products.findOne() //每个产品都有它自己的文档,每个文档的parts数组中将会存放多个零件的ObjectId
{
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
]
}

你需要一个应用层级别的join操作才能获取特定产品的所有零件。

// Fetch the Product document identified by this catalog number
> product = db.products.findOne({catalog_number: 1234});
// Fetch all the Parts that are linked to this Product
> product_parts = db.parts.find({_id: { $in : product.parts } } ).toArray() ;

为了能快速的执行查询,必须确保products.catalog_number有索引。
当然由于零件中parts._id一定是有索引的,所以这也会很高效。
这种引用的方式是对内嵌优缺点的补充。
每个零件是个独立的文档,可以很容易的去查询和更新他们。
使用这种建模方式的一个不利因素是:需要一条单独的语句去获取一个产品的零件中的具体内容。
(请仔细思考这个问题,在第二章反范式化中,我们还会讨论这个问题)
这种建模方式中的零件部分可以被多个产品使用,
所以你的一对N建模就自动变成了N对N建模,不像Sql一样还需要一张连接表。

初级: 一对非常多

以事件日志系统为例,即使你存储在数组里的只有ObjectId,
但任何一台主机都能产生大量的信息来超出文档的16MB大小的限制。
这就是父引用的经典用例–用一个文档存储主机,在每个日志文档中保存这个主机的ObjectID。

> db.hosts.findOne()
{
_id : ObjectID('AAAB'),
name : 'goofy.example.com',
ipaddr : '127.66.66.66'
}
>db.logmsg.findOne()
{
time : ISODate("2014-03-28T09:42:41.382Z"),
message : 'cpu is on fire!',
host: ObjectID('AAAB') // Reference to the Host document
}

你将会用一个略有不同的应用层级别的join操作来获取到某个主机的最近5000条信息。

// find the parent ‘host’ document
> host = db.hosts.findOne({ipaddr : '127.66.66.66'}); // assumes unique index
// find the most recent 5000 log message documents linked to that host
> last_5k_msg = db.logmsg.find({host: host._id}).sort({time : -1}).limit(5000).toArray()

总结

即使在基础层面,你也能察觉出mongobd的建模和关系模型建模的不同之处。
你必须注意两个因素:
1、一对N中的N是否需要一个单独的实体?
2、这种一对N关系中的N到底是什么样的规模?是一对少,一对多,还是一对很多。
基于以上因素来决定采取何种建模方式:
一对少且不需要在父对象外的上下文中访问内嵌对象的情况下使用内嵌。
一对多或多的一端内容因为各种理由需要单独存在的情况下使用数组引用。
一对非常多的情况下使用父引用。
下一篇文章中我们将会看到如何使用双向关系和反范式化去提升以上三种基本方案的性能。