这一篇文档描述了在 Mongo 中的 CRUD 操作。透过最常用的增删改查来快速了解 MongoDB 。
插入操作
插入文档的语法如下:
db.<collection>.[insertOne|insertMany]({
key: value
}, {}..., {writeConcern: <document>, ordered: <boolean>});
其中,writeConcern表示安全写,如果没有指定,则是默认写。 而ordered字段则表示无序写入,则 Mongo 会优化写入的速度。
指定插入命令之后,返回的响应如下:
{"ackownledged": true, "insertedId": "xxx"}
如果我们没有通过_id指定主键,那么会自动生成唯一的主键序列。如果制定了重复的主键,则会返回错误:
E11000 duplicate key error collection: test.accounts.index: _id_ dup key...
我们可以使用ObjectId()函数来创建对象主键,对象主键中也包含了文档的创建时间,我们可以通过ObjectId('5db42e...').getTimestamp() 来获取文档的创建时间。此外,我们可以创建一个复合主键,指定一个文档作为主键:
db.<collection>.insert({_id: {type: "saveings", accountNo: "001"}})
另外,创建文档也可以使用 insert命令,这个命令和前两个命令的区别是它会根据你你可以传入单个或者多个文档,另外这个命令支持explain查看执行计划,而insertOne和insertMany不支持。另外,还可以使用save命令,但是这个命令底层也是调用insert命令的,所以返回一致。
查询文档
查询文档我们使用find命令,它的语法如下:
db.<collection>.find(<query>, <projection>)
其中, query 指的是查询的条件,而projection则定义了对读取结果的投射(你可以理解为过滤查询字段)。
查询全部文档:
db.<collection>.find() // 可以使用 find().pretty() 来美化输出结果
查询指定数量的文档:
db.<collection>.find().batchSize(20)
指定条件查询:
db.<collection>.find({key1: value1, key2: value2}) // 默认为 and
// 查询复合主键
db.<collection>.find({"_id.key": "value"})
// 使用比较操作符, $eq \ $ne \ $gt \ $gte \ $lt \ $lte
db.<collection>.find({key: {$eq: value}})
// 另外,还有两个常用的比较操作符:分别是 $in 和 $nin, 查询包含或者不包含指定值的情况
db.<collection>.find({key: {$in: [<value1>, <value2>...<valueN>]}})
⚠️ : 在使用 $ne 或者 $nin 操作符的时候,, 那么即使文档中不包含你指定的字段,也会查询出来。
操作符号还包括逻辑操作符, 如下表所示:
| 操作符 | 描述 |
|---|---|
| $not | 匹配(一个)筛选条件不成立的文档 |
| $and | 匹配多个筛选条件全部成立的文档 |
| $or | 匹配至少一个筛选条件成立的文档 |
| $nor | 匹配多个筛选条件全部不成立的文档 |
示例如下:
db.<collection>.find({ $and: [ {key: { $gt: value} }, {key, {$lt: "value"}} ] })
查询指定字段是否存在:
db.<collection>.find( {key: { $exists: true }} ) // 查询存在 key 字段的文档
查询字段的类型:
db.<collection>.find( {key: {$type: "string"}}). // 查询 key 的类型为 string 的文档
查询数组操作符,分为为 $all和$elemMatch:
db.<collection>.find( { key: {$all: [<value1>, <value2> ...<valueN>]}}) // 包含所有指定的数组值
db.<collection>.find( { key: {$elemMatch: [<value1>, <value2> ...<valueN>]}}) // 包含任意的数组值
使用正则表达式来查询:
db.<colletion>.find({name: {$in: [ /^c/, /^j/]}}) // 查询 c 或者 j 开头的姓名
db.<collection>.find({name: {$regex: /LIE/, $options: 'i' }}) // i 表示不区分大小写,大部分使用这样的语法,除非用 in
默认返回前面 20 条数据,查询数量:
db.<collection>.find().count()
查询单条数据:
db.<collection>.findOne()
有些字段是对象,可以采用嵌套查询:
db.<collection>.find({
"imdb.rating": 6.2
})
查询评分少于 6.2 的电影:
db.<collection>movies.find({
"imdb.rating": {$lt: 6.2}
})
再来看看如何进行排序:
db.<collection>.find().sort({key1: -1, key2: 1})
注意⚠️: 当
skip、limit、sort命令一起执行的时候,需要注意顺序,sort会在limit和skip之前执行,skip在limit之前执行。
最后再来看看文档的投影:
db.<collection>.find({}, { key: 1 }) // 值返回文档中的 key, 和文档主键
db.<collection>.find({}, { key: 0 }) // 不返回文档中的 key
注意⚠️:除了文档主键其他的不能混用包含和不包含两种操作,要么使用包含,要么使用不包含。
在涉及到数组的时候,投影可能会更加的复杂。看如下示例:
db.<collection>.find({}, {key: { $slide: 1}}). // key 是一个数组,使用 $slide 只返回它的第一个元素,也可以使用 -1 -2
db.<collection>.find({}, {key: { $elemMatch: {$gt: "value"}}}) // 返回数组只能够第一个满足大于指定的值的元素
更新文档
更新文档使用update命令,具体的语法如下:
db.<collection>.[updateOne|updateMany](<query>, <update>, <options>)
其中,query定义了查询的条件,即满足条件的文档会被更新;update表示文档更新的内容;options是更新的一些选项配置。例如下面的示例更新了用户的名称:
db.tags.updateOne({name: "Tom"}, {
$set: {name: "Bob"},
})
注意:文档的主键
_id是不能够被更改的。
默认情况下,只会更新一篇文档,即使是查询到条件满足多篇文档也是如此。除非我们在 options 文档中指定 multi 参数为 true ,才会更新多篇文档。
注意: MongoDB 只能保障单个文档更新的原子性,而不能保证多个文档更新时候的原子性。更新多个文档的操作苏联在单一线程中执行,但是线程在执行过程中可能会被挂起,以便其他线程也有机会对数据进行操作,在这个过程中挂起线程中的文档就可能会其他线程改变。如果需要保障多个文档的更新的原子性,就需要使用 4.0 版本开始提供的事务特性。
默认情况下,如果更新中指定的查询条件不被任何文档满足,就不会更新任一文档。但是我们可以通过options文档中的upsert 字段来改变这样的默认行为,即使不满足任何条件,也会创建文档。
如果我们使用 db.<collection>.save(<document>) 命令的时候,如果当中包含了 _id 字段,就会调用db.<collection>.update 来更新我们的文档,否则则会创建文档。
基本操作符
上文中,类似于$set这样的被称之更新操作符。类似的如下所示:
| 操作符 | 描述 | 操作符 | 描述 |
|---|---|---|---|
$set |
更新或者新增字段 | $unset |
删除字段 |
$rename |
重命名字段 | $inc |
加减字段 |
$mul |
相乘字段值 | $min |
比较减小字段值 |
$max |
比较增大字段值 |
我们在来看看$unset 操作符的示例:
db.tags.updateOne({name: "Bob"}, {
$unset: {
status: "这里写什么都可以,不影响删除这个字段",
}
})
注意: 如果
$unset的字段在文档中不存在,则不会执行更新操作。另外,需要注意,如果$unset的是一个数组元素,那么数组的元素会被设置为null, 但是数组的长度不会被改变。
接着我们再来看看$rename 操作符的示例:
db.tags.updateMany({name: "Bob"}, {
$rename: {
name: "nickname"
}
})
语法都几乎是一样的,但是有一点需要注意,如果并不是更新整个集合的所有文档,那么并不会删除这个被 rename 的字段,而是会将其值置为 null, 并添加新字段。如果所有的文档的字段都被统一 rename,则原字段会被删除。如果$rename的字段不存在,则不会更新文档。如果字段已经存在,则已经存在的字段下的值会被 $rename 的值覆盖。
另外,$rename 操作也可以用来改变文档的结构,比如插入如下文档:
db.products.insertOne({
name: "iPhone 12 Pro Max",
price: 9200.00,
details : {
main_img: "http://dv-test.cn/img/202005240102.jpg",
sku: {memory: "6GB", storage: "128GB"}
}
})
如果我希望把 .details.main_img 提升到 .main_img ,把 .price 放到 .detail 当中去呢?如下实例:
db.products.updateOne({name: "iPhone 12 Pro Max"}, {
$rename: {
price: "details.price",
"details.main_img": "main_img"
}
})
注意:
$rename来改变文档字段结构的时候,更新前和更新后的字段都不能是数组内的元素。
和其他数据库的更新操作不一样,Mongo 还增加了 $min 和 $max 这两个比较更新操作符。顾名思义,先行比较,比较之后根据比较结果的大小来决定是否更新。我们以$min来举例:
db.products.updateOne({name: "iPhone 12 Pro Max"}, {
$min: {
"details.price": 10000.00,
}
})
这条更新操作并不会真得更新。因为我们得产品的价格是 9200, 小于我们更新的价格,因为 $min 操作符的缘故,所以不会更新大于原始值的值。如果换成$max 操作符,就会更新。**类似于$inc、$min、$max 这样的操作符,如果更新的字段并不存在,则仍旧会更新。另外,如果更新后的字段类型和原来的字段类型不同,会根据 Mongo 默认的比较规则来更新。规则如下:
| 最小 | 最大 | |||||||||
|---|---|---|---|---|---|---|---|---|---|---|
| Null | Numbers | Symbol、String | Object | Array | BinData | ObjectId | Boolean | Date | Timestamp | Regular Expression |
数组操作符
我们来说一些更新数组的操作符:
| 操作符 | 描述 | 操作符 | 描述 |
|---|---|---|---|
$addToSet |
向数组中添加元素 | $pop |
从数组中移除元素 |
$pull |
从数组中有选择性移除元素 | $pullAll |
从数组中有选择性地移除元素 |
pull |
向数组中添加元素 |
首先,我们来演示 $addToSet ,如果指定的 key 不存在则会创建。下面的示例中,我们为 products集合中的 iPhone 12 Pro Max添加类目信息,它是一个数组:
db.products.updateOne({name: "iPhone 12 Pro Max"}, {
$addToSet: {
categories: ["Phone", "Apple"]
}
})
这里需要注意的是,如果你添加的字段的值的顺序不同,也会被作为新的内容插入,比如说你把categories的值改成["Apple", "Phone"] 也会被当作新的记录插入。
那么如果我希望往数组中添加一个值呢?将["Phone", "Apple"]改成["Phone", "Apple", "2021", "Hot"]呢?可以使用$push操作符:
db.products.updateOne({name: "iPhone 12 Pro Max"}, {
$push: {
"categories.0": {
$each: ["2021", "Hot"],
$position: 0, $sort: 1, $slice: -8
}
}
})
如果你不加上$each操作符的话,["2021", "Hot"] 整个数组都会被作为一个元素插入到原有的数组中去。而 $position 操作符的作用是将元素添加到指定位置,接受负值。而 $sort 操作符是为了给更新后的数组排序的(1 升序、-1 降序),$slice 操作符可以对更新后数组进行截取。但**$slice**** 和 **$sort** 操作符必须和 **$each** 和 **$push** 配合使用。不管书写的顺序是什么,但是这几个操作符的执行允许永远是先****$position**** 后 **$sort** 最后 **$slice**。**
既然有$push就会有对应的$pop, 删除一个元素,示例如下:
db.products.updateOne({name: "iPhone 12 Pro Max"}, {
$pop: {
"categories.1": -1,
}
})
使用$pop操作符清除数组中最后一个元素之后,并不会清除数组本身,而是保留空数组。另外,不要将$pop运用在非数组类型的字段上。和 $pop 相比,我们可以使用更加灵活的$pull 操作符:
db.products.updateOne({name: "iPhone 12 Pro Max"}, {
$pull: {
"categories.0": { $regex: /20/ },
}
})
上面的代码中,我们使用了正则来查找出匹配的数组元素,并删除,所以相比 $pop 而言更加的灵活、强大。除了$pull操作符外,还有一个$pullAll操作符,可以指定多个匹配的值,然后删除全部匹配的内容。下面两个命令达到的效果是一样的:
{ $pullAll: { key: [value1, value2]}}
{ $pull: {key: {$in: [value1, value2]}}}
但是需要注意的是,使用**$pullAll**操作符,必须完全匹配文档的内容,遇到数组、对象这样的类型的时候必须完全匹配。而使用**$pull**操作符则不必完全匹配。
占位符
我们来看下面的语句来说明占位符的作用:
db.products.updateOne({ name: "iPhone 12 Pro Max", categories: ["Phone"]}, {
$set: {
"categories.$": ["Spring"]
}
})
这句当中的 $ 符号就是占位符,表示的是更新数组的第一个符合条件 元素。看上面的示例,categories.$ 指代的就是在符合我们的查询条件 categories: ["Phone"] 的第一个值,它将被更新为 ["Spring"]。
使用 $ 占位符更新的只是符合条件的第一个值,那么如果我希望更新所有符合条件的值呢?可以使用占位符 $[], 这样就不需要指定条件了,如下:
db.products.updateOne({ name: "iPhone 12 Pro Max"}, {
$set: {
"categories.$[]": ["Spring!"]
}
})
删除文档
删除文档相比之前的操作来说简单多了,可以使用 deleteOne|deleteMany命令,语法格式如下:
db.<collection>.[deleteOne|deleteMany](<query>, <options>)
其中query 文档用以指定查询条件,而options 用以指定删除的一些配置选项。例如我们要删除之前产品的文档:
db.products.deleteOne({ name: "iPhone 12 Pro Max"})
删除所有文档,可以不指定参数:
db.products.deleteMany({})
如果我们需要删除整个集合,可以使用 drop 命令:
db.products.drop()