mongodb设计模式

在上一篇文章中我介绍了三种基本的设计方案:内嵌,子引用,父引用,
同时说明了在选择方案时需要考虑的两个关键因素。
1、一对N中的N是否需要一个单独的实体?
2、这种一对N关系中的N到底是什么样的规模?是一对少,一对多,还是一对非常多。
在掌握了以上基础技术后,我将会介绍更为复杂的设计模式:双向引用和反范式化。

中级: 双向引用

如果你想让你的设计更酷,你可以让引用的“one”端和“many”端同时引用对方。
以上一篇文章讨论过的任务追踪系统为例,
有一个people集合保存Person文档,
有一个tasks集合保存Task文档,
有一个One-to-N关系是从Person -> Task。
这个应用需要追踪一个人的所有任务,所有我们需要引用Person->Task。

db.people.findOne() //注意此处原文为:db.person.findOne(),原文是错误的。
{
_id: ObjectId("AAF1"), //注意此处原文为:ObjectID,正确的写法应该是ObjectId。
name: "Kate Monster",
tasks [ // array of references to Task documents
ObjectId("ADF9"),
ObjectId("AE02"),
ObjectId("AE73")
// etc
]
}

在某些场景中这个应用需要显示任务的列表(例如显示一个多人协作项目中所有的任务),
并且你需要快速找到每个任务对应的负责人,
你可以在Task文档添加一个额外的引用到Person,Task->Person。

db.tasks.findOne()
{
_id: ObjectID("ADF9"),
description: "Write lesson plan",
due_date: ISODate("2014-04-01"),
owner: ObjectID("AAF1") // Reference to Person document
}

这个设计具有所有One-to-Many方案的优点和缺点,
在Task文档添加额外的owner引用可以快速而且简单地找到任务的主人,
但同时也意味着如果你需要重新分配这个任务给其他人,你需要执行两处更新而不是一处。
明确地说,你需要更新Person->Task的引用,还需要更新Task->Person的引用。
对于那些熟悉关系型数据库的同学来说(没错,说的就是你),
双向引用这个建模设计意味着不可能进行原子型更新。
这对于我们的这个任务追踪系统来说是OK的,
但对于你自己的特别用例你需要考虑下是否合适。

中级: 在一对多关系中应用反范式

在你的设计中加入反范式,可以使你避免应用层级别的join读取。
当然,代价是这也会让你在更新时添加一些复杂度,下面我会举例子说明。

反范式化:从多到一

在零件那个例子中,你可以反范式零件的名字到parts[]数组。
这是没有范式化的产品文档:

> 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
]
}

反范式化意味着当你需要显示某个产品的所有零件时,你不需要执行应用层级别的join操作。
但是如果你需要关于一个零件的更多信息时,你还是需要执行join操作。

> db.products.findOne()
{
name : 'left-handed smoke shifter',
manufacturer : 'Acme Corp',
catalog_number: 1234,
parts : [
{ id : ObjectID('AAAA'), name : '#4 grommet' },// Part name is denormalized
{ id: ObjectID('F17C'), name : 'fan blade assembly' },
{ id: ObjectID('D2AA'), name : 'power switch' },
// etc
]
}

当你轻易就能获取到零件的名字时,这会增加一些客户端的工作来实现应用层级别的join。

// Fetch the product document
> product = db.products.findOne({catalog_number: 1234});
// Create an array of ObjectID()s containing *just* the part numbers
> part_ids = product.parts.map( function(doc) { return doc.id } );
// Fetch all the Parts that are linked to this Product
> product_parts = db.parts.find({_id: { $in : part_ids } } ).toArray() ;

反范式化在节省你读的代价同时会增加你更新的代价:
如果你将零件的名字冗余到产品文档中,那么当你更新零件的名字时,
你必须更新产品集合里的每一个有这个零件名字的文档。
反范式化仅仅在读比写频率高的场景下才有意义。
如果你经常读冗余数据,但却很少更新数据,即使付出的代价是慢更新,或者复杂更新,
就是为了方便查询,那么反范式化是有意义的。
更新的频率越高,反范式化的带来的好处就越少。
举例:假如零件的名字很少变化,但是零件的库存就经常变化。
那就意味着将零件的名字冗余到产品文档是有意义的,反之是没有意义的。
记住:如果你冗余一个字段,你就丢失了在这个字段上原子更新和独立更新的能力。
就像上面的双向引用例子,如果你更新了零件文档的零件名字,那么在产品文档,
就会存在毫秒级的间隔,产品文档的零件名字将不会反射零件文档里更新过的值。

反范式化:从一到多

同样你可以冗余one端的数据到many端

> db.parts.findOne()
{
_id : ObjectID('AAAA'),
partno : '123-aff-456',
name : '#4 grommet',
product_name : 'smoke shifter', // Denormalized from the ‘Product’ document
product_catalog_number: 1234, // Ditto
qty: 94,
cost: 0.94,
price: 3.99
}

如果你冗余产品的名字到零件文档,那么当你更新产品名字的同时你必须更新零件集合中每一个包含产品名字的零件文档。
这看起来是一个代价更高的更新,因为你在更新多个零件而不是更新仅仅一个产品。
因此,这种情况下更应该慎重的考虑读写频率。

中级:在一对很多关系中应用反范式化

在日志系统这个一对很多的例子中也可以应用反范式化的技术。
你可以将one端(主机对象)冗余到日志对象中,或者反之。
下面的例子将主机中的IP地址冗余到日志对象中。

> db.logmsg.findOne()
{
time : ISODate("2014-03-28T09:42:41.382Z"),
message : 'cpu is on fire!',
ipaddr : '127.66.66.66',
host: ObjectID('AAAB')
}

如果想获取最近某个ip地址的日志信息就变的很简单,一条语句就能搞定,而之前需要两条。

> last_5k_msg = db.logmsg.find({ipaddr : '127.66.66.66'}).sort({time : -1}).limit(5000).toArray()