MongoDB学习笔记六:进阶指南

时间:2023-03-03 14:19:56

【数据库命令】
『命令的工作原理』
MongoDB中的命令其实是作为一种特殊类型的查询来实现的,这些查询针对$cmd集合来执行。runCommand仅仅是接受命令文档,执行等价查询,因此,
> db.runCommand({"drop" : "test"})
这个drop调用实际上是这样的:
db.$cmd.findOne({"drop" : "test"})
当MongoDB服务器得到查询$cmd集合的请求时,会启动一套特殊的逻辑来处理,而不是交给普通的查询代码来执行。几乎所有MongoDB驱动程序都提供与一个类似于runCommand的帮助方法来执行命令,但是如果有必要,总是可以使用一个简单查询的方式来运行命令。
访问有些命令需要有管理员权限,必须在admin数据库里面运行。如果在别的数据库里运行这样的命令,会得到“拒绝访问”的错误。
『命令参考』
要活的所有命令的最新列表,有两种方式:
·在shell中运行db.listCommands(),或者从驱动程序中运行等价的命令listCommands。
·浏览管理员接口http://localhost:28017/_commands
MongoDB中最经常使用的命令:
· buildInfo
{"buildInfo" : 1}
管理专用命令,返回MongoDB服务器的版本号和主机的操作系统
· collStats
{"collStats" : collection}
返回指定集合的统计信息,包括数据大小、已分配的存储空间和索引的大小。
· distinct
{"distinct" : collection, "key" : key, "query" : query}
列出指定集合中满足查询条件的文档的指定键的所有不同值。
· drop
{"drop" : collection}
删除集合的所有数据。
· dropDatabase
{"dropDatabase" : 1}
删除当前数据库的所有数据。
· dropIndexes
删除集合里面名称为name的索引,如果名称为"*",则删除全部索引。
· findAndModify
更新文档,并且返回更新前或者后的文档信息。(这一行自己总结的)
· getLastError
查看对本集合执行的最后一次操作的错误信息或者其他状态信息。
· isMaster
{"isMaster" : 1}
检查本服务器是猪服务器还是从服务器。
· listCommands
{"listCommands" : 1}
返回所有在服务器上运行的命令及相关信息。
· listDatabases
{"listDatabases" : 1}
管理专用命令,列出服务器上所有的数据库。
· ping
{"ping" : 1}
检查服务器链接是否正常。即便服务器上锁了,这条命令也会立刻返回。
· renameCollection
{"renameCollection" : 1, "to" : b}
将集合a重命名为b,其中a和b都必须是完整的集合命名空间(例如"foo.bar"表示foo数据库中的bar集合)
· repairDatabase
{"repairDatabase" : 1}
修复并压缩当前数据库,这个操作可能非常耗时。
· serverStatus
{"serverStatus" : 1}
返回这台服务器的管理统计信息。
【固定集合】——固定大小的集合
固定集合要实现创建,而且大小固定。
固定集合很像环形队列,如果空间不足,最早的文档就会被删除,为新的文档腾出空间。这意味着固定集合在新文档插入的自动淘汰最早的文档。
有些操作不适用于固定集合。不能删除文档(除了前面说的自动淘汰),更新不得导致文档移动(通常更新意味着尺寸增大)。
基于上述两点,可以保证在固定集合中的文档以插入的顺序存储,而且不必维护一个已删除的文档的释放空间列表。
固定集合在默认情况侠没有索引,即便是"_id"上也没有索引。
『属性及用法』
①对固定一盒插入速度极快。因为不需要额外分配空间。
②按照插入顺序输出的查询速度极快。
③固定集合能够在新数据插入时,自动淘汰最早的数据。
插入快速、按照插入顺序查询也快速、自动淘汰 --> 固定集合特别适合像日志这种应用场景。
事实上,MongoDB中设计固定集合的目的就是用来存储内部的复制日志oplog。
『创建固定集合』
不像普通集合,固定集合必须要在使用前显式地创建。使用create命令创建。在shell中,可以使用createCollection来创建:
> db.createCollection("my_collection", {capped: true, size: 100000});
{ "ok" : true }
上面的命令创建了一个固定集合my_collection,大小是100000字节。createCollection也有别的选项。除了指定总的容量,还可以指定文档数量的上限:
> db.createCollection("my_collection", {capped: true, size: 100000, max: 100});
{ "ok" : true }
注:当指定文档数量上限时,必须同时指定大小。淘汰机制只有在容量还没有满时才回一句文档数量来工作。要是容量满了,淘汰机制则会依据容量来工作,就想别的固定集合一样。
还可以通过转换已有的普通几核的方式来创建固定集合。使用convertToCapped命令来完成这个操作。下面的例子中,会把test集合转换成大小为10000自己的固定集合:
> db.runCommand({convertToCapped: "test", size: 10000});
{ "ok" : true }
『自然排序』
使用自然排序按照插入顺序或者反向插入顺序返回文档。
> db.my_collection.find()/sort({"$natural": -1})
使用{"$natural" : 1}表示与默认顺序相同。
『尾部游标』
尾部游标是一种特殊的持久游标,这类游标不会在没有结果后销毁。游标收到tailf命令的启发,类似地会尽可能持续地获取结果输出。因为这类游标在没有结果后也不销毁,所以一旦有新文档添加到集合里面就会被取回并输出。尾部游标只能用在固定集合上。
Mongo shell不支持尾部游标,但是可以看看PHP中的例子:

$cursor = $collection->find()->tailable();

while (true) {
if (!$cursor->hasNext()) {
if ($cursor->dead()) {
reak;
}
sleep(1);
}
else {
while (cursor->hasNext()) {
do_stuff(cursor->getNext());
}
}
}

游标没有销毁,要么处理结果,要么等着有更多的结果。
【GridFS:存储文件】
『开始使用GridFS:mongofiles』
mongofiles内置在MongoDB发布版中,可以用来在GridFS中上传、下载、列示、查找或删除文件。
执行mongofiles --help可以查看可用选项。
下面将会介绍如何用mongofiles从文件系统向GridFS上传文件,列出GridFS中的所有文件,下载刚上传的文件:
$ echo "Hello, world" > foo.txt
$ ./mongofiles put foo.txt
connected to: 127.0.0.1
added file: { _id: ObjectId('4c0d2a6c3052c25545139b88'),
filename: "foo.txt", length: 13, chunkSize: 262144,
uploadDate: new Date(1275931244818),
md5: "a7966bf58e23583c9a5a4059383ff850" }
done!
$ ./mongofiles list
connected to: 127.0.0.1
foo.txt 13
$ rm foo.txt
$ ./mongofiles get foo.txt
$ cat foo.txt
Hello, world
上面的例子中,使用了mongofiles的3个基本操作:put、list和get。put将文件系统中的一个文件添加到GridFS中,list会把所有添加到GridFS中的文件列出来,get则是put的逆操作,它将GridFS中的文件写入到文件系统中。mongofiles还支持另外两个操作:search用来按文件名查找GridFS中的文件,delete则从GridFS中删除一个文件。
『通过MongoDB驱动程序操作GridFS』
例:使用MongoDB的Python驱动程序PyMongo,可以实现上面用mongofiles执行的一系列操作:
>>> from pymongo import Connection
>>> import gridfs
>>> fs = Connection().test
>>> fs = gridfs.GridFS(db)
>>> file_id = fs.put("Hello, world", filename="foo,txt")
>>> fs.list()
[u'foo.txt']
>>> fs.get(file_id).read()
'Hello, world'
『内部原理』
GridFS的一个基本思想就是可以将大文件分成很多块,每块作为一个单独的文档存储,这样就能村大文件了。由于MongoDB支持在文档中存储二进制数据,可以最大限度减小块的存储开销。另外,除了存储文件本身的块,还有一个单独的文档用来存储分块的信息和文件的元数据。
GridFS的块有个单独的集合。默认情况下,块将使用fs.chunk集合,如有需要可以覆盖。这个块集合里面文档的结构是非常简单的:
{
"_id" : ObjectId("..."),
"n" : 0,
"data" : BinData("..."),
"files_id" : ObjectId("...")
}
和别的MongoDB文档一样,快也有自己唯一的"_id"。
"files_id"是包含这个块元数据的文件文档的"_id"。
"n"表示块编号,也就是这个块在源文件中的顺序编号。
"data"包含组成文件块的二进制数据。
文件的元数据放在另一个集合中,默认是fs.files。这里面的每个文档代表GridFS中的一个文件,与文件相关的自定义元数据也可以存在其中。
除了用户自定义的键,GridFS规范还定义了一些键:
_id
文件唯一的id,在块中作为"files_id"键的值存储。
length
文件内容总的字节数。
chunkSize
每块的大小,以字节为单位。默认是256K,必要时可以调整。
uploadDate
文件存入GridFS的时间戳。
md5
文件内容的md5校验和,由服务器端生成。
理解了GridFS规范后,实现一些驱动程序没提供的功能就很容易了。例如,可以使用distinct命令获取GridFS中不重复的文件名列表:
> db.fs.files.distinct("filename")
[ "foo.txt" ]
【服务器端脚本】
在服务器端可以通过db.eval函数来执行JavaScript脚本。也可以把JavaScript脚本保存在数据库中,然后再别的数据库命令中调用。
『db.eval』
利用db.eval可以在MongoDB的服务器端执行任意的JavaScripe脚本。
这个函数先将给定的JavaScript字符串传送给MongoDB(在这里执行),然后返回结果。
db.eval可以用来模拟多文档事务:db.eval锁住数据库,然后执行JavaScript,再解锁。
发送代码有两种选择,或者封装进一个函数,或者不封装。下面两行代码是等价的:
> db.eval("return 1;")
1
> db.eval("function() { return 1; }")
1
只有传递参数的时候,才必须要封装成一个函数。参数通过db.eval的第二个参数传递,要写成一个数组的形式。例如,如果想给一个函数传递username,可以这么做:
> db.eval("function(u) { print('Hello, ' + u + '!'); }", [username])
有必要的话可以传递多个参数。例如,要计算3个数的和,可以这样:
> db.eval("function(x,y,z) { return x + y + z; }", [num1, num2, num3])
num1对应x,num2对应y,num3对应z。
调试db.eval的一个方法是:将调试信息写进数据库日志中,这个可以通过print函数来完成:
> db.eval("print('Hello, world');");
『存储JavaScript』
每个MongoDB的数据库中都有个特殊的集合,叫做system.js,用来存放JavaScript变量。这些变量可以在任何MongoDB的JavaScript上下文中调用,包括"$where"自居,db.eval调用,MapReduce作业。用insert就可以将变量加进system.js中。
> db.system.js.insert({"_id" : "x", "value" : 1})
> db.system.js.insert({"_id" : "y", "value" : 2})
> db.system.js.insert({"_id" : "z", "value" : 3})
上例在全局作用于中定义了x、y、z。现在要是想对其求和,可以这样:
> db.eval("return x+y+z;")
6
除了一些简单的值,system.js也可以用来存放JavaScript代码。这样就可以很方便地自定义一些实用程序。例如,要用JavaScript写一个日志函数,就可以将其存放到system.js中:

> db.system.js.insert({"_id" : "log", "value" :
function(msg, level) {
var levels = ["DEBUG", "WARN", "ERROR", "FATAL"]
level = level ? level : 0; // check if level is defined
var now = new Date();
print(now + " " + levels[level] + msg);
}})

现在,可以在任意的JavaScript程序中调用这个函数:
> db.eval("x = 1; log('x is '+x); x = 2; log('x is greater than 1', 1);");
数据库日志会含有类似下面的内容:
Fri Jun 11 2010 11:12:39 GMT-0400 (EST) DEBUG x is 1
Fri Jun 11 2010 11:12:40 GMT-0400 (EST) WARN x is greater than 1
使用存储的JavaScript缺点就是代码会与常规的源代码控制脱离,会搅乱客户端发送来的JavaScript。
最适合使用存储的JavaScript的情况就是程序中有多个地方(也可能是不同的程序,或者不同语言的代码)都要用到一个JavaScript函数。将这样的函数放置在中心位置,要是有更新的话就不必每处都修改。要是JavaScript代码很长又要频繁使用的话,也可以使用存储的JavaScript,这样存一次会节省不少网络传输时间。
『安全性』
若是想打印"Hello, 用户名!"给用户。其中的用户名保存在一个名为username的变量中。可以像下面这样写这段程序:
> func = "fucntion() { printf('Hello, "+username+"!'); }"
如果username是用户定义的,就可能会是这样的字符串"'); db.dropDatabase(); print('",这样代码就变成了下面这样:
> func = "fucntion() { printf('Hello, '); db.dropDatabase(); print('!'); }"
整个数据库都被清干净了!
为了避免这种情况,要先定作用域。例如,在PHP中应该这样写:
$func = new MongoCode("function() { print('Hello, "+username+"!'); }", array("username" => $username));
数据可就会安全地输出如下字符:
Hello, '); db.dropDatabase(); print('!
【数据库引用】
数据库引用,也叫作DBRef。DBRef就像URL,唯一确定一个到文档的引用。它自动加载文档的方式正如网站中URL通过链接自动加载Web页面一样。
『什么是DBref』
DBRef是个内嵌文档,就像MongoDB中的其他内嵌文档一样。但是DBref有些必选键。下面是一个简单的例子:
{"$ref" : collection, "$id" : id_value}
DBRef指向一个集合,还有一个id_value用来在集合里面根据"_id"确定唯一的文档。这两条信息使得DBRef能够唯一标识MongoDB数据库内的任何一个文档。若是想引用另一个数据库中的文档,DBRef中有一个可选键"$db",用这个就可以了:
{"$ref" : collection, "$id" : id_vlue, "$db" : database}
注:DBRef中的键的顺序不能改变,第一个必须是"$ref",接着是"$id",然后是(可选的)"$db"。
『示例模式』
来看一个使用DBRef跨集合引用文档的例子:
本例中含有两个集合users和notes。用户(user)可以创建笔记(note),笔记可以引用用户或者别的笔记。现在有一些用户文档,每一个都有唯一的用户名作为其"_id",以及一个独立行事的"display_name":
{"_id" : "mike", "display_name" : "Mike D"}
{"_id" : "kristina", "display_name" : "Kristine C"}
notes集合稍微复杂一些。每个笔记都含有一个唯一的"_id"。正常情况下,这个"_id"很可能是个ObjectID,但试着利用整数,是为了让例子简明,突出重点。notes还有一个"author",若干个"text",以及一个可选的"references"指向其他笔记或者用户:
{"_id" : 5, "author" : "mike", "text" : "MongoDB is fun!"}
{"_id" : 20, "author" : "kristina", "text" : "... and DBRefs are easy, too",
"references" : [{"$ref" : "users", "$id" : "mike"}, {"$ref" : "notes", "$id" : 5}]}
第二个笔记包含一些对其它文档的引用,每一条都作为一个DBRef存储。应用层的程序会利用这些DBRef得到用户"Mike"和笔记"MongoDB is fun!"这两个文档,而它们都是与Kristina的笔记关联的。去引用是很容易实现的。"$ref"的值就是要查询的集合,然后使用"$id"键的值,获得"_id"的值:

> var note = db.notes.findOne({"_id" : 20})
> note.references.forEach(function(ref) {
printjson(db[ref.$ref].findOne("_id" : ref.$id));
});
{ "_id" : "Mike", "display_name" : "Mike D" }
{ "_id" : 5, "author" : "mike", "text" : "MongoDB is fun!" }

『驱动对DBRef的支持』
不是所有驱动程序都将DBRef作为普通的内嵌文档。一些驱动程序为DBRef提供了特殊的类型,这样就会和普通文档自动相互转换。这主要是为了开发者提供便利,因为这样可以忽略少量细节。例如,使用PyMongo中的DBRef类型可以向下面这样表示上面的例子:

>>> note = {"_id" : 20, "author" : "kristina",
"text" : "... and DBrefs are easy, too",
"references" : [DBRed("user", "mike"), DBRef("notes", 5)]}

当保存时,DBRef实例会自动被转换成等价的内嵌文档。当作为查询结果返回时,逆操作也会自动进行,就又得到了DBRef的实例。
一些驱动程序还添加了别的驱动工具来操作DBRef,比如处理去引用的方法,甚至提供当返回结果包含引用时自动去引用的机制。这些辅助功能随驱动程序的不同而变化,要想知道最新的信息,需要参考具体驱动程序的文档。
『什么时候该使用DBRef』
存储一些对不同几核的文档的引用时,最好使用DBRef。或者想使用驱动程序或者工具中DBRef特有的功能,只能用DBRef了。
否则,最好存储"_id"作为引用来使用,因为这样更精简,也更容易操作。