一、MongoDB 基础知识
1. 文档
文档是 MongoDB 的核心概念。文档就是键值对的一个有序集。每种编程语言表示文档的方法不大一样,但大多数编程语言都有一些相通的数据结构,比如映射(map)、散列(hash)或字典(dictionary)。例如,在 JavaScript 里面,文档被表示为对象:
{"greeting" : "Hello, world!"}
这个文档只有一个键 “greeting”,其对应的值为 “Hello, world!”。大多数文档会比这个简单的例子复杂得多,通常会包含多个键/值对:
{"greeting" : "Hello, world!", "foo" : 3}
从上面的例子可以看出,文档中的值可以是多种不同的数据类型(甚至可以是一个完整的内嵌文档)。
MongoDB 不但区分类型,而且区分大小写。例如,下面的两个文档是不同的:
{"foo" : 3}
{"foo" : "3"}
下面两个文档也是不同的:
{"foo" : 3}
{"Foo" : 3}
还有一个非常重要的事项需要注意, MongoDB 的文档不能有重复的键。例如,下面的文档是非法的:
{"greeting" : "Hello, world!", "greeting" : "Hello, MongoDB!"}
2. 集合
集合就是一组文档。如果将 MongoDB 中的一个文档比喻为关系型数据库中的一行,那么一个集合就相当于一张表。
集合是动态模式的。这意味着一个集合里面的文档可以是各式各样的。例如,下面两个文档可以存储在同一个集合里面:
{"greeting" : "Hello, world!"}
{"foo" : 5}
需要注意的是,上面的文档不光值的类型不同(一个是字符串,一个是整数),他们的键也完全不同。因为集合李阿敏可以放置任何文档。
3. 数据库
在 MongoDB 中,多个文档组成集合,而多个集合可以组成数据库。
另外,有一些数据库名是保留的,可以直接访问这些有特殊语义的数据库。这些数据库如下所示。
- admin。从身份验证的角度来讲,这是“root” 数据库。如果将一个用户添加到 admin 数据库,这个用户将自动获得所有数据库的权限。再者,一些特定的服务器端命令也只能从 admin 数据库运行,如列出所有数据库或关闭服务器。
- local。这个数据库永远都不可以复制,且一台服务器上的所有本地集合都可以存储在这个数据库中。
- config。MongoDB 用于分片设置,分片信息会存储在 config 数据库中。
二、创建、更新和删除文档
1. 插入
1.1 命令
insert 和 insertMany。
1.2 举例
单条数据插入
> db.blog.insert({"author":"tian","title":"my first mongodb blog"})
WriteResult({ "nInserted" : 1 })
>
查询结果:
> db.blog.find()
{ "_id" : ObjectId("500bb4b44daafbf976598437"), "author" : "tian", "title" : "my first mongodb blog" }
数据批量插入
> var res = db.collection.insertMany([{"b": 3}, {'c': 4}])
> res
{
"acknowledged" : true,
"insertedIds" : [
ObjectId("571a22a911a82a1d94c02337"),
ObjectId("571a22a911a82a1d94c02338")
]
}
1.3 说明
1)当我们要插入的集合(这里是blog)不存在时,mongodb会在第一次插入时自动创建一个;
2)插入的每一条文档,除了我们制定的键(这里有2个键,author和title),还会自动增加一个_id键,相当于关系型数据库的主键,如果我们没有指定的话。
该键对于一个集合必须是唯一的,它可以使任意类型,默认是ObjectId对象。
由于mongodb一开始设计就是用来作为分布式数据库的,因此没有采用自增长的方式来创建_id键,因为在不同的服务器上同步自增长主键费时又费力。
关于ObjectId,更多可以参考:http://www.mongodb.org/display/DOCS/Object+IDs
当然我们也可以自己指定:
> db.blog.insert({"_id":2012,"author":"tian","title":"my 2nd mongodb blog"})
> db.blog.find()
{ "_id" : ObjectId("500bb4b44daafbf976598437"), "author" : "tian", "title" : "my first mongodb blog" }
{ "_id" : 2012, "author" : "tian", "title" : "my 2nd mongodb blog" }
注意:findOne 也可以用来查询文档,只不过 find 查询的是所有的数据,返回的是一个数组对象; findOne 查出来的是查到的第一个对象。
1.4 插入原理
当我们将数据插入到mongodb数据库时,数据会被转换成BSON的形式(BSON是mongodb存储数据的形式,类似JSON的,是轻量的二进制格式,能将mongodb的所有文档表示为字节字符串,数据库能理解BSON,存在磁盘上的文档也是BSON格式,更多请参考: http://www.bsonspec.org/),然后存入数据库。
在这个阶段,mongodb只检查2件事:
- 1)是否包含_id键;
- 2)文档是否超过16M,注意,这里的大小是转成BSON格式以后的大小,可以通过Object.bsonsize(your-object)查看大小;(以mongodb 1.8为准)
只要这两点满足,就会将文档原本的存入到数据库
这样做有好处也有副作用,副作用就是可以插入无效的数据,好处就是可以使数据库更安全,远离注入式攻击,因为插入不执行代码。
2. 删除
2.1 命令
remove
2.2 举例:
> db.blog.remove({"title":"my first mongodb blog"})
WriteResult({ "nRemoved" : 1 })
> db.blog.remove()
WriteResult({ "nRemoved" : 2 })
2.3 说明:
- 在上面的2个例子中,第一个例子的remove接受一个参数,用于限定要删除的文档,第二个例子没有指定参数,注意,这种操作是很危险的,因为它将删除整个集合里的文档,但是集合以及索引会被保留,第二个例子等价于db.blog.remove({})
- 删除是永久性的,不能撤销,也不能恢复
- 删除文档的速度相当快,如果要删除整个集合里的文档,可以采用db.your-collection.drop(),然后重建集合和索引,该方法速度非常快,唯一的缺点是整个集合都被删除了,包括索引
- 根据_id键来删除文档的效率是最高的
- 考虑一种比较极端,或者高并发可能发生的情况,当你要删除一个集合里的文档时,刚好有另一个进程在update其中的文档,在这种情况下,正被update的文档是不会被删除的,如果这并不是你想要的,可以通过制定参数KaTeX parse error: Expected '}', got 'EOF' at end of input: …author":"tian",atomic:true}),当然,这样做也是有副作用的,就是当我们执行remove操作的时候,将阻止其他操作。
3. 更新
3.1 命令
update
3.2 举例
> var mypost = db.blog.findOne({"_id":ObjectId("500bc4304daafbf976598439")})
> mypost.author="tian.chen"
tian.chen
> mypost
{
"_id" : ObjectId("500bc4304daafbf976598439"),
"author" : "tian.chen",
"title" : "my first mongodb blog"
}
> db.blog.update({"_id":ObjectId("500bc4304daafbf976598439")},mypost)
> db.blog.findOne({"_id":ObjectId("500bc4304daafbf976598439")})
{
"_id" : ObjectId("500bc4304daafbf976598439"),
"author" : "tian.chen",
"title" : "my first mongodb blog"
}
3.3 说明
- 1)我们首先通过findOne来获取一个文档,并赋值给mypost,然后,修改mypost的author键,最后再通过db.blog.update更新文档
- 更新时匹配多个文档,更新的时候,由于第二个参数的存在就会产生重复的_id键,就会报错。怎么说呢,举个例子
> db.blog.find()
{ "_id" : ObjectId("500bc4304daafbf976598439"), "author" : "tian.chen", "title": "my first mongodb blog", "age" : 28 }
{ "_id" : 2012, "author" : "tian.chen", "title" : "my 2nd mongodb blog", "age" : 6 }
> var tianpost=db.blog.findOne({"author":"tian.chen","age":6})
> tianpost
{
"_id" : 2012,
"author" : "tian.chen",
"title" : "my 2nd mongodb blog",
"age" : 6
}
> tianpost.age=26
26
> db.blog.update({"author":"tian.chen"},tianpost)
cannot change _id of a document old:{ _id: ObjectId('500bc4304daafbf976598439'),
author: "tian.chen", title: "my first mongodb blog", age: 28.0 } new:{ _id: 201
2.0, author: "tian.chen", title: "my 2nd mongodb blog", age: 26.0 }
在这个例子中,我们的post集合里包含2个文档,我们要将第二个文档的age改成26,首先通过author和age获取一个文档并赋值给tianpost,在这里,tianpost也是具有_id键的,其值为2012,然后,我们将tianpost的age改成26后,然后通过author=tian.chen查找文档,将其update为tianpost,问题就出现在这里,通过author=tian.chen查找文档时,首先找到第一个文档,其_id为 ObjectId(‘500bc4304daafbf976598439’),而tianpost._id=2012,我们知道_id是不能修改的,结果就报错了!
为了避免这种情况,最好确保更新总是指定唯一文档。
3.4 使用修改器
在mongodb中通常文档只会有一部分要更新,利用原子的更新修改器,可以做到只更新文档的一部分键值,而且更新极为高效,更新修改器是种特殊的键,用来指定复杂的更新操作,比如调整、增加、或者删除键,还可以操作数组和内嵌文档。增加、修改或删除键的时候,应该使用$修改器。
要把"foo"的值设备"bar",常见的错误做法如下:
db.coll.update(criteria,{"foo":"bar"})
这种情况是不对的,实际上这种做法会把整个文档用{“foo”:“bar”}替换掉,一定要使用以$开头的修改器来修改键/值对
假设有这样一个文档:
{
"_id" : 2012,
"age" : 26,
"author" : "tian.chen",
"pageviews" : 1,
"title" : "my 2nd mongodb blog"
}
其中的键pageviews表示该post被阅读的次数,每阅读一次就增加1,这时,我们可以使用$inc修改器
> db.blog.update({"_id":2012},{"$inc":{"pageviews":1}})
3.4.1 $set修改器
$set修改器用来指定一个键的值,如果这关键不存在就创建它,这对于修改来说是很方便的,因为我们通常修改的只是极个别的键的值的。
假设我们要修改下面文档的age:
{
"_id" : 2012,
"age" : 26,
"author" : "tian.chen",
"pageviews" : 1,
"title" : "my 2nd mongodb blog"
}
如果我们只是简单的用db.blog.update({"_id":2012},{“age”:28}),那么,将会用{“age”:28}替换掉整个文档,这个时候,$set修改器就很有用处了:
db.blog.update({"_id":2012},{"$set":{"age":28}})
当然,也可以修改多个:
db.blog.update({"_id":2012},{"$set":{"age":28,"author":"tian"}})
用unset可以删除键,如:
db.blog.update({"_id":2012},{"$unset":{"pageviews":1}})
这样,就会将文档中的pageviews键删除掉
3.4.2 $inc修改器
正如我们在上面看到的,$inc修改器用来增加键的值.
需要注意的是inc的键的值必须为数字,如果要修改其他类型,应使用上面的$set
3.4.3 数组修改器 $push & $pop
如果指定的键存在,$push就会向已有的数据末尾加入一个元素,如果没有,则会创建一个新的数组
如:
>db.blog.update({"_id":2012},{$push:{"comments":{"name":"jake","content":"nice post"}}})
查出来的结果为:
> db.blog.find()
{ "_id" : ObjectId("500bc4304daafbf976598439"), "author" : "tian.chen", "title": "my first mongodb blog", "age" : 28 }
{ "_id" : 2012, "age" : 28, "author" : "tian", "comments" : [ { "name" : "jake", "content" : "nice post" } ], "title" : "my 2nd mongodb blog" }
有时候,我们可能会希望,如果一个值在数组中不存在,就添加进去,可以用如下方式来实现,即通过$ne来实现:
> db.papers.insert({"title":"mongodb post","authors":["tian"]})
> db.papers.update({"authors":{"$ne":"harry"}},{$push:{"authors":"harry"}})
{ "_id" : ObjectId("500c0098886e42d4a1a1bff5"), "authors" : [ "tian", "harry" ], "title" : "mongodb post" }
也可用ne并不总是可行:
> db.papers.update({"_id": ObjectId("500c0098886e42d4a1a1bff5")},
... {"$addToSet":{"authors":"jerry"}})
>{ "_id" : ObjectId("500c0098886e42d4a1a1bff5"), "authors" : [ "tian", "harry", "jerry" ], "title" : "mongodb post" }
each结合,可以插入多个值,当然,如果值已经在数组中,就不会被添加进去:
> db.papers.update({"_id": ObjectId("500c0098886e42d4a1a1bff5")},
... {"$addToSet":
... {"authors":{"$each":["tian","jerry","mike"]}}})
{ "_id" : ObjectId("500c0098886e42d4a1a1bff5"), "authors" : [ "tian", "harry", "
jerry", "mike" ], "title" : "mongodb post" }
“tian”,"jerry"已经存在,因此没有重复添加,"mike"不存在,被添加进来了。
$slice
还可以在添加数组时限制长度,可以使用 $slice 这样可以得到一个最多包含N个元素的数组
> db.blog.posts.update({"title": "A Blog Post"}, {"$push": {"comment": {"$each": [{"1": "a"},{"2": "b"},{"3": "c"},{"7": "d"},{"6": "f"}], "$slice": -2}}})
WriteResult({ "nMatched" : 1, "nUpserted" : 0, "nModified" : 1 })
> db.blog.posts.find()
{ "_id" : ObjectId("5ccd977c7dd247457f95a421"), "title" : "A Blog Post", "content" : "...", "author" : { "name" : "joe", "email" : "joe@example.com" }, "comment" : [ { "7" : "d" }, { "6" : "f" } ] }
> db.blog.posts.update({"title": "A Blog Post"}, {"$push": {"comment": {"$each": [{"1": "a"},{"2": "b"},{"3": "c"},{"7": "d"},{"6": "f"}], "$slice": -10}}})
WriteResult({ "nMatched" : 1, "nUpserted" : 0, "nModified" : 1 })
> db.blog.posts.find()
{ "_id" : ObjectId("5ccd977c7dd247457f95a421"), "title" : "A Blog Post", "content" : "...", "author" : { "name" : "joe", "email" : "joe@example.com" }, "comment" : [ { "7" : "d" }, { "6" : "f" }, { "1" : "a" }, { "2" : "b" }, { "3" : "c" }, { "7" : "d" }, { "6" : "f" } ] }
可以看到当slice的值必须是负整数。
$sort
db.blog.posts.update({"title": "A Blog Post"}, {"$push": {"comment": {
"$each": [{"1": "a", "rating": 6.6},{"2": "b", "rating": 7.9},{"3": "c", "rating": 9.0}],
"$slice": -10,
"$sort": {"rating": -1}}}})
这样会根据"rating"字段的值对数组中所有的对象进行排序, “sort"必须与”$push"配合使用
删除数组的元素可以用pull
KaTeX parse error: Expected '}', got 'EOF' at end of input: …要用于删除数组头部或尾部的值{pop:{key:-1}}和{$pop:{key:1}}
> db.papers.find()
{ "_id" : ObjectId("500c0098886e42d4a1a1bff5"), "authors" : [ "tian", "harry", "jerry", "mike" ], "title" : "mongodb post" }
> db.papers.update({"_id": ObjectId("500c0098886e42d4a1a1bff5")},
... {$pop:{"authors":1}}
... )
> db.papers.find()
{ "_id" : ObjectId("500c0098886e42d4a1a1bff5"), "authors" : [ "tian", "harry", "jerry" ], "title" : "mongodb post" }
> db.papers.update({"_id": ObjectId("500c0098886e42d4a1a1bff5")},
... {$pop:{"authors":-1}})
> db.papers.find()
{ "_id" : ObjectId("500c0098886e42d4a1a1bff5"), "authors" : [ "harry", "jerry" ], "title" : "mongodb post" }
可以发现{KaTeX parse error: Expected 'EOF', got '}' at position 13: pop:{key:-1}}̲删除头部,而{pop:{key:1}}删除尾部的。
更经常的,我们希望通过值来判断,这时候就可以用KaTeX parse error: Expected '}', got 'EOF' at end of input: pull,{"pull":{key:value}}
3.4.4 数组的定位修改器 $
若是数组有多个值,而我们只想对其中一部分进行操作,有两种方法可以实现这种操作。
两种方法操作数组中的值:通过位置或定位操作符("$")
数组都是以0开头的,可以将下标直接作为键来选择元素。
> db.blog.findOne()
{
"_id" : ObjectId("57709da84f533aa7535d46d3"),
"title" : "a blog post",
"comments" : [
{
"name" : "joe",
"email" : "joe@example.com",
"content" : "nice post"
},
{
"name" : "bob",
"email" : "bob@example.com",
"content" : "good post"
}
]
}
> db.blog.update({"title":"a blog post"},{$set:{"comments.1.name":"livan"}})
WriteResult({ "nMatched" : 1, "nUpserted" : 0, "nModified" : 1 })
> db.blog.findOne()
{
"_id" : ObjectId("57709da84f533aa7535d46d3"),
"title" : "a blog post",
"comments" : [
{
"name" : "joe",
"email" : "joe@example.com",
"content" : "nice post"
},
{
"name" : "livan",
"email" : "bob@example.com",
"content" : "good post"
}
]
}
在很多情况下,不预先查询文档就不能知道要修改数组的下标,为了克服这种困难,mongodb提供了定位操作符"$",用来定位查询文档已经匹配的元素,并进行更新,定位符只更新第一个匹配的元素。
例如:用户john把名字改成了jim,就可以用定位符来替换评论中的名字:
db.blog.update({"comments.author":"john"},{$set:{"comments.$.author:"john"}})
可以理解为{“comments.author”:“john”}查询条件定位到第一个元素,就执行{KaTeX parse error: Expected '}', got 'EOF' at end of input: set:{"comments..author:“john”}},"$"定位符就表示找到的第一个元素
> db.blog.findOne()
{
"_id" : ObjectId("57709da84f533aa7535d46d3"),
"title" : "a blog post",
"comments" : [
{
"name" : "joe",
"email" : "joe@example.com",
"content" : "nice post"
},
{
"name" : "livan",
"email" : "livan@example.com",
"content" : "good post"
}
]
}
> db.blog.update({"comments.name":"livan"},{$set:{"comments.$.email":"bob@example.com"}})
WriteResult({ "nMatched" : 1, "nUpserted" : 0, "nModified" : 1 })
3.5 upsert更新
upsert是一种特殊的更新,要是没有符合更新条件的文档,就会以这个条件和文档为基础创建一份新的文档。如果有匹配的文档,则正常更新。
假设我们有一个集合analytics,用来记录每个url的访问次数,每访问一次就给pageviews键加1,正常情况,我们需要判断当前访问的url有没有存在,如果没有,则添加,有则更新。
采用upsert,我们可以有更优雅的写法:
db.analytics.update({"url":"/blog"},{"$inc":{"pageviews":1}},true)
在这里,我们通过给update传递第三个参数表示upsert
3.6 save函数
save跟upsert有点类似,也是不存在时插入,存在时更新。不同的是save只有一个参数。看下面的例子:
save方法有更新和插入两种功能,到底是插入还是更新文档取决于save的参数。那么到底是依赖于哪个参数呢?继续看
If the document does not contain an _id field, then the save() method calls the insert() method. During the operation, the mongo shell will create an ObjectId and assign it to the _id field.
可以看到决定是插入一个文档还是更新,取决于_id参数。如果能根据_id找到一个已经存在的文档,那么就更新。如果没有传入_id参数或者找不到存在的文档,那么就插入一个新文档。
举一个官方的例子
不带_id参数
db.products.save( { item: "book", qty: 40 } )
结果
{ "_id" : ObjectId("50691737d386d8fadbd6b01d"), "item" : "book", "qty" : 40 }
MongoDb客户端驱动会自动为你生成一个默认ObjectId作为_id。
带_id参数,但是找不到一个已经存在的文档
db.products.save( { _id: 100, item: "water", qty: 30 } )
结果
{ "_id" : 100, "item" : "water", "qty" : 30 }
还是插入一个新文档,但是_id不会自动生成。
带_id参数,但是有存在的文档
db.products.save( { _id : 100, item : "juice" } )
结果
{ "_id" : 100, "item" : "juice" }
更新了文档
3.7 更新多个文档
在前面的例子中,我们都是只更新一个文档,mongodb目前也是默认只更新一个文档。我们可以通过对update指定第四个参数,来更新多个文档
在上面的例子中,我们对比了没传第四个参数和传第四个参数进行更新的区别,传第四个参数后,符合条件的都会更新。
更新完成后,可以通过db.runCommand({getLastError:1})来获取更新了多少文档。