mongodb指南(翻译)(二十五) - developer zone - 插入对象(二)模式设计(Schema Design)
合法关键字名称
文档中的关键字在命名时需要遵循下面两个限制条件:
- "$"字符不能作为关键字的第一个字符
- "."字符不能被用于关键字中
模式设计(Schema Design)
Mongodb中的模式设计与关系型DBMS大不相同。在创建应用前很有必要来了解一下mongodb中的模式设计。
在关系型数据模型中,对于一个给定依赖于用例的关系模型,理论上是有一个正确的设计。这就是通常的第三范式(3NF).一般人认为这是为了效率考虑。
在Mongodb中,模式设计不仅仅是一个将数据模型化的方法,更是用例的模型化。对于绝大多数的用例,模式设计需要单独定制。这有优点和缺点-用例可以得到高效的执行;但是它可能是有盲点的模式,在特定查询下没有关系型模式那么优雅。
当我们设计一个模式,有一些问题我们必须回答的是:
- 相对于链接,什么时候我们将数据内嵌(embed)? 我们的决定会暗示问题2的答案:
- 我们需要多少集合,它们都是什么?
- 何时需要原子操作?这些操作仅可以在一个BSON文档范围内执行,不能跨文档操作。
- 我们会创建那些索引,以使查询和更新更快?
- 我们怎样进行分片?分片的key又是什么?
内嵌和链接
当设计一个Mongodb模式,一个关键问题就是何时使用内嵌,何时使用链接。内嵌是将对象和数组内嵌到BSON文档里面。链接是文档间的引用。
在mongodb中是没有关联(join)的 - 在一个有着1000个服务器集群中分发关联是非常困难的。内嵌有点类似“预关联”数据。在一个文档内部的操作对于服务器来说很容易处理;这些操作可以非常丰富。相对的,链接必须在客户端进行处理;应用通过发起进一步的查询来完成这些操作。
通常来说,为了“保持”实体间的关系,应该选择使用内嵌。在没有使用链接时,再使用链接可能会带来数据的复制。
集合
mongodb中的集合类似于关系型数据库中的表。每个集合保存了很多文档。正如前面提到的,这些文档的内容可以非常丰富。
一个集合中的文档的字段不需要显示声明。但是对于模式设计师来说,他应该有个概念这些字段是什么并且这些文档在集合中如何被结构化。Mongodb不需要一个集合中的文档拥有相同的结构。但是,在实际中,绝大多数集合内部的文档都是同一类结构的。虽然在我们需要时,我们可以移走这些(一个集合的文档拥有相同的结构);例如我们增加一个新的字段。在这种情况下,类似于“alter table”的操作不是必须的。
原子操作
有些问题需要执行原子操作来解决。例如,对计数器加1就是常见的需要原子操作的一种用例。Mongodb还可以执行很复杂的原子操作:
atomically {
if( doc.credits > 5 ) {
doc.credits -= 5;
doc.debits += 5;
}
}
另外的例子可能是用户注册流程。我们绝不允许用户注册两个相同的用户名:
atomically {
if( exists a document with username='jane' ) {
print "username already in use please choose another";
} else {
insert a document with username='jane' in the users collection;
print("thanks you have registered as user jane.");
}
}
这里的关键是原子操作的范围/ACID内容就是这个文档。这样的话我们就需要保证所有在原子操作中涉及的字段都属于同一个文档。
索引
Mongodb支持声明索引。mongodb中的索引同关系型数据库中的索引非常类似:它们为高效查询而设计,必须显式声明。这样的话,作为模式设计的一部分,我们需要考虑定义那些索引。同关系型数据库一样,索引可以随后再增加 - 如果我们决定将来会增加新的索引,我们可以随后再加。
分片
另外一个在模式设计时需要考虑的就是分片了。一个BSON文档(它可能有一些重要内嵌字段)仅会驻留在一个分片上面。
一个集合可以被分片。当被分片,这个集合会有一个分片关键字,它决定该集合如何分片到集群中。
通常(不是必须)对分片集合的查询会包含分片关键字。
需要注意的是,分片后再更改分片关键字非常困难。
例子
让我们来考虑一个内容管理系统的例子。下面的例子我们使用的mongo shell语法,但是它应该也可以在任何编程语言中实现 - 只要使用合适的驱动就可以了。
我们的内容管理系统会保存博客。博客有作者。我们还要支持对博客进行评论和投票。我们还希望增加标签来提供搜索功能。
一个好的模式设计可能会使用两个mongodb集合,一个叫做posts,另一个叫users.这就是我们的例子中要使用的。
我们的用户有一些属性 - 注册时使用的用户ID,名称,和他们的karma。例如我们可以:
> db.users.insert( { _id : "alex", name: { first:"Alex", last:"Benisson" }, karma : 1.0 } )
_id字段在mongodb中必须存在,并且会自动为它建立有唯一属性的索引。这非常适合使用我们的用户名作为_id.虽然我们不是必须如此;我们也可以增加用户名字段,然后让系统自动生成_id字段。
现在让我们来考虑博客。我们假定已经有一些博客发布了。让我们查询一篇:
> db.posts.findOne()
{
_id : ObjectId("4e77bb3b8a3e000000004f7a"),
when : Date("2011-09-19T02:10:11.3Z",
author : "alex",
title : "No Free Lunch",
text : "This is the text of the post. It could be very long.",
tags : [ "business", "ramblings" ],
votes : 5,
voters : [ "jane", "joe", "spencer", "phyllis", "li" ],
comments : [
{ who : "jane", when : Date("2011-09-19T04:00:10.112Z"),
comment : "I agree." },
{ who : "meghan", when : Date("2011-09-20T14:36:06.958Z"),
comment : "You must be joking. etc etc ..." }
]
}
对比一下在关系型数据中如何实现这种模式,你会发现很有趣。我们会首先有一个用户集合和博客集合。但是通常还需要增加标签集合,投票集合,和评论集合。收集一篇博客的全部信息可能会有些麻烦。
这里获取一个完整博客信息,我们可以:
> db.posts.findOne( { _id : ObjectId("4e77bb3b8a3e000000004f7a") } );
查询由“alex”撰写的所有博客:
> db.posts.findOne( { author : "alex" } )
如果上面的查询使用的很多,我们可以对作者字段建立索引:
> db.posts.ensureIndex( { author : 1 } )
整个博客文档可能会很大。如果仅仅获取alex发表博客的标题,可以这样:
> db.posts.findOne( { author : "alex" }, { title : 1 } )
按标签进行搜索:
> // make and index of all tags so that the query is fast:
> db.posts.ensureIndex( { tags : 1 } )
> db.posts.find( { tags : "business" } )
查询meghan所有的评论:
> db.posts.find( { comments.who : "meghan" } )
可以建立索引加快搜索:
> db.posts.ensureIndex( { "comments.who" : 1 } )
我们希望一个人对一篇博客仅能投票一次。下面的更新例子会记录calvin的投票。由于使用了$nin子表达式,如果calvin已经投过票了,这个更新将不会更新数据库:
> db.posts.update( { _id : ObjectId("4e77bb3b8a3e000000004f7a"),
voters : { $nin : "calvin" } },
{ votes : { $inc : 1 }, voters : { $push : "calvin" } );
上面的操作是原子的:如果多个用户同时投票,没有投票会被丢失。
假定我们希望显示最新的博客标题和作者。这种用例需要用到客户端联接:
> var post = db.posts.find().sort( { when : -1 } ).limit(1);
> var user = db.users.find( { _id : post.author } );
> print( post.title + " " + user.name.first + " " + user.name.last );
posted on 2012-02-21 14:02 xinghebuluo 阅读(1786) 评论(0) 编辑 收藏 举报