文档主键
文档主键 _id
文档主键 _id
是每篇文档必备的字段,具有以下特性:
- 文档主键的唯一性
- 支持所有数据类型(数组除外)
- 复合主键
对象主键 ObjectId
当我们不提供主键,MongoDB
自动为我们生成的默认对象主键 ObjectId
- 默认的文档主键
- 可以快速生成的
12
字节id
- 包含创建时间
ObjectId
默认值
1 | ObjectId() |
手动设置 ObjectId
的值
1 | ObjectId("123456789011123456789011") |
提取 ObjectId
的创建时间
1 | ObjectId("123456789011123456789011").getTimestamp() |
复合主键
使用文档作为文档主键
举个例子:
1 | db.accounts.insert({ |
复合主键仍然要满足文档主键的唯一性,需要字段值和顺序完全一致才算重复。
下面是不同的 _id
:
1 | _id: {accountNo: "001", type: "savings"} |
1 | _id: { type: "savings", accountNo: "001"} |
创建文档
创建单个文档
1 | db.<collection>.insertOne( |
<collection>
: 文档集合名<document>
: 准备写入数据库的文档Json
文档格式1
2
3
4
5{
_id: "account1",
name: "alice",
balance: 100
}
writeConcern
: 文档定义了本次文档创建操作的安全写级别简单来说,安全写级别用来判断一次数据库写入操作是否成功。安全写级别越高,丢失数据的风险就越低,然而写入操作的延迟也可能更高。如果不提供writeConcern
文档,MongoDB
使用默认的安全写级别
举个例子:
1 | db.accounts.insertOne( |
insertOne
、insetMany
、insert
会自动创建相应集合_id
字段不能重复,省略创建文档中的_id
字段,会自动生成_id
。
创建多个文档
1 | db.<collection>.insertMany( |
ordered
:MongoDB
是否按照顺序来写入这些文档。默认为true
。
举个例子:
1 | db.accounts.insertMany( |
错误处理
在顺序写入时,一旦遇到错误,操作便会退出,剩余的文档无论正确与否,都不会被写入。
在乱序写入时,即使某些文档造成了错误,剩余的正确文档仍然会被写入
创建单个或多个文档
1 | db.<collection>.insert( |
insert
命令既可以提供单个 document
也可以提供 document
数组, 返回结果不同, 正确返回 WriteReult
对象
举个例子:
1 | db.accounts.insert( |
返回结果:
1 | WriteResult({ "nInserted": 1 }) |
三个命令的区别
- 返回结果不同
insertOne
和insertMany
命令不支持db.collection.explain()
命令,insert
命令支持。
save
1 | db.<collection>.save( |
当 db.collection.save()
命令处理一个新文档的时候,会调用 db.collection.insert()
命令
读取文档
1 | db.<collection>.find(<query>, <projection>) |
<query>
: 查询文档<projection>
: 定义了对读取结果进行的投射
读取全部文档
既不筛选,也不使用投射
1 | db.accounts.find() |
格式友好:
1 | db.accounts.find().pretty() |
筛选文档
匹配查询
1 | db.accounts.find({name: "alice", balance: 100}) |
嵌入文档的查询:
1 | db.accounts.find({"_id.type": "savings"}) |
比较操作符
常用的比较操作符包括:
$eq
匹配字段值相等的文档$ne
匹配字段值不等的文档- 也会筛选出不包含查询字段的文档
$gt
匹配字段值大于查询值的文档$gte
匹配字段值大于或等于查询值的文档$le
匹配字段值小于查询值的文档$lte
匹配字段值小于或等于查询值的文档
操作命令格式:
1 | { <field>: {$<operator>: <value> }} |
举个例子:
读取 alice
的文档:
1 | db.accounts.find({ name: { $eq: "alice" }}) |
读取余额大于 100
的文档:
1 | db.accounts.find({ balance: { $gt: 100 }}) |
$in
匹配字段值与任一查询值相等的文档$nin
匹配字段值与任何查询值都不相等的文档- 也会筛选出不包含查询字段的文档
操作命令格式:
1 | { <field>: { $in: [<value1>, <value2>, ...] }} |
举个例子:
读取 alice
和 charlie
的银行账户文档:
1 | db.accounts.find({ name: { $in: ["alice", "charlie"] }}) |
逻辑操作符
$not
匹配筛选条件不成立的文档- 操作命令格式:
1
{ <field>: { $not: {<operator-expression> } }}
- 也会筛选出不包含查询字段的文档
- 操作命令格式:
$and
匹配筛选条件全部成立的文档- 操作命令格式:
1
{ $and: [{<operator-expression1>}, {<operator-expression2>}, ...]}
- 操作命令格式:
$or
匹配筛选条件至少一个成立的文档- 操作命令格式:
1
{ $or: [{<operator-expression1>}, {<operator-expression2>}, ...]}
- 当所有的筛选条件都是
$eq
操作符时,$or
和$in
效果一样
- 操作命令格式:
$nor
匹配多个筛选条件全部不成立的文档- 操作命令格式:
1
{ $nor: [{<operator-expression1>}, {<operator-expression2>}, ...]}
- 也会筛选出不包含查询字段的文档
- 操作命令格式:
举个例子:
读取余额不小于 500
的银行账户文档:
1 | db.accounts.find( |
读取余额大于 100
且用户姓名排在 fred
之后的银行账户文档:
1 | db.accounts.find( |
当筛选条件应用在不同字段上时,可以省略 $and
操作符, 上面等价于:
1 | db.accounts.find( |
当筛选条件应用在同一字段上时,也可以省略 $and
操作符。例如:
查询余额大于 100
并且小于 500
的银行账户:
1 | db.accounts.find( |
字段操作符
$exists
- 查询包含字段值的文档
- 操作命令格式:
1
{ field: { $exists: <boolean> }}
举个例子:
读取文档主键 _id.type
存在并且不等于 checking
1 | db.accounts.find( |
$type
- 查询包含字段值类型的文档
- 操作命令格式:
1
2{ field: { $type: <BSON type> }}
{ field: { $type: [<BSON type1>, <BSON type2>, ... ]}}
举个例子:
读取文档主键是字符串的银行账户文档
1 | db.accounts.find( |
读取文档主键是对象主键或者是复合主键的银行账户文档
1 | db.accounts.find( |
也可以使用 BSON
类型序号作为 $type
操作符的参数
1 | db.accounts.find( |
数组操作符
$all
- 筛选数组中包含所有的查询值的文档
- 操作命令格式:
1
{ field: { $all: [<value1>, <value2>, ... ]}}
$elemMatch
- 筛选数组中任一一个元素满足查询条件
- 操作命令格式:
1
{ field: { $elemMatch: {<query1>, <query2>, ... }}}
举个例子
1 | db.accounts.find( |
$all
可以 与 $elemMatch
结合使用:
读取包含一个在 1000
至 2000
之间和一个在 2000
至 3000
之间的联系电话的银行账户文档:
1 | db.accounts.find( |
运算操作符
$regex
正则表达式,操作命令格式:
1 | { <field>: { : /pattern/, : '<options>'}} |
兼容 PCRE v8.41
正则表达式库
在和 $in
操作符一起使用时,只能使用 /pattern/<options>
方式
举个例子:
读取用户姓名以 c 或者 j 开头的银行账户文档:
1 | db.accounts.find({name: {$in: [ /^c/, /^j/ ]}}) |
读取用户姓名包含 ALI
(不区分大小写)的银行账户文档:
1 | db.accounts.find({name: {$regex: /ALI/, $options: "i"}}) |
文档游标
db.collection.find()
返回一个文档集合游标
在不迭代游标的情况下,只列出前 20
个文档
1 | var myCursor = db.accounts.find(); myCursor |
可以使用游标下标直接访问文档集合中的某一个文档
1 | var myCursor = db.accounts.find(); myCursor[0] |
遍历完游标中所有的文档之后,或者在 10
分钟之后,游标会自动关闭, 可以使用 noCursorTimeout()
函数来保持游标一直有效
1 | var myCursor = db.accounts.find().noCursorTimeout() |
主动关闭游标
1 | myCursor.close() |
游标函数
- cursor.hasNext()
- cursor.next()
- cursor.forEach()
- cursor.limit()
limit(0)
相当于没有limit
- cursor.skip()
- cursor.count(
) - 默认情况下
applySKipLimit
为false
, 即count
不会考虑skip
和limit
的效果 - 不提供筛选条件时,
count
会从集合的元数据Metadata
中取得结果,当数据库分布式结构较为复杂时,元数据中的文档数量可能不正确,这时更建议使用聚合管道来计算文档数量
- 默认情况下
- cursor.sort(
) document
定义排序要求,1
表示正向排序,-1
表示逆向排序{field: ordering}
不管命令顺序,执行的顺序永远是 sort > skip > limit
文档投影
1 | db.<collection>.find(<query>, <projection>) |
不使用投影时, find
返回符合条件的完整文档,而使用投影可以有选择性的返回文档中的部分字段
projection
格式:
1 | { filed: inclusion } |
1
表示返回字段,0
表示不返回字段, 没有设置主键默认会返回
举个例子:
只返回银行账户的用户姓名
1 | db.accounts.find({}, {name: 1}) |
只返回银行账户的用户姓名(不包含主键)
1 | db.accounts.find({}, {name: 1, _id: 0}) |
除了文档主键外,不可以在投影文档上混合使用包含和不包含这两种投影操作
🙅
1 | db.accounts.find({}, {name: 1, balance: 0}) |
数组字段上使用投影
$slice
$slice
操作符可以返回数组字段中的部分元素
1 | {$slice : n| -n | [i, j]} |
n
: 数组前n
个元素-n
: 数组后n
个元素[i, j]
: 跳过i
个元素,返回接下来j
个元素
只返回 contact
数组中的第一个元素
1 | db.accounts.find({}, {_id : 0, name: 1, contact: {$slice: 1}}) |
$eleeMatch
& $
$eleeMatch
和 $
操作符返回数组中满足筛选条件的第一个元素
1 | db.accounts.find({}, {_id : 0, name: 1, contact: {$elemMatch: {$gt: "Alabama"}}}) |
与下等同:
1 | db.accounts.find({contact: { $gt: "Alabama"}}, {_id : 0, name: 1, contact: "contact.$: 1"}) |
更新文档
1 | db.collection.update(<query>, <update>, <options>) |
<query>
筛选条件<update>
更新文档<options>
设置参数
更新整篇文档
如果 <update>
文档不包含任何更新操作符,db.collection.update()
将会使用 <update>
文档直接替换集合中符合 <query>
文档筛选条件的文档
1 | db.account.update({name:"alice" }, {name : "alice", "balance":123}) |
注意问题:
- 文档主键
_id
是不可以更改的 - 只有第一篇符合
<query>
文档符合筛选条件的文档会被更新
更新特定字段
更新文档特定字段,需要使用到文档更新操作符,如下:
$set
更新或新增字段$unset
删除字段$rename
重命名$inc
加减字段值$mul
相乘字段值$min
比较减小字段值$max
比较增大字段值
$set
1 | { $set: { <filed1>: <value1>, ...}} |
举个例子:
1 | db.accounts.update( |
更新或新增内嵌文档的字段
1 | db.accounts.update( |
更新或新增数组字段
在 $set
中使用数组名加下标,如果向现有数组字段范围以外的位置添加新值,数组字段的长度会被扩大,未被赋值的数组成员将被设置为 null
举个例子 jack
的 contact
数组中默认有 3
个元素:
更新
jack
的contact
数组中第一个元素1
2
3
4
5
6
7
8
9db.accounts.update(
{name: "jack"},
{
$set:
{
"contact.0": "666666"
}
}
)更新
jack
的contact
数组中第四元素在
$set
中使用数组名加下标3
, 默认只有3
个,这个操作会在数组后边新增一个元素.1
2
3
4
5
6
7
8
9db.accounts.update(
{name: "jack"},
{
$set:
{
"contact.3": "77777"
}
}
)更新
jack
的contact
数组中第六个元素在
$set
中使用数组名加下标5
, 现在只有4
个,这个操作会在数组后边新增两个元素,第五个元素为null
值,第六个为新增值1
2
3
4
5
6
7
8
9db.accounts.update(
{name: "jack"},
{
$set:
{
"contact.5": "99999"
}
}
)
$unset
删除字段, 传入任何值都一样,默认设置为 ""
, 格式如下:
1 | { $unset: {field1: "", ...}} |
例如删除 jack
的银行账户余额和开户地点:
1 | db.accounts.update( |
- 删除内嵌文档的字段
- 同更新内嵌文档字段方式
- 删除数组内的字段
- 同更新内嵌文档字段方式。删除数组中元素成功时,这个元素不会被删除,只会被赋以
null
值,而数组的长度不会改变。
- 同更新内嵌文档字段方式。删除数组中元素成功时,这个元素不会被删除,只会被赋以
如果 $unset
命令中的字段不存在,那么文档内容将保持不变
$rename
重命名文档字段
1 | { $rename: {<field1>: <newName1>, <field2>:<newName2>, ...}} |
- 如果
$rename
命令中的字段不存在,那么文档内容将保持不变 - 如果新的字段名已经存在,那么原有的这个字段会被覆盖
当 $rename
命令中的新字段存在时,$rename
命令会先 $unset
旧字段,然后再 $set
新字段
- 重命名内嵌文档的字段
- 普通重命名字段名
- 同更新内嵌文档字段方式
- 更新内嵌文档中字段的位置
1
2
3
4
5
6
7
8
9
10db.accounts.update(
{name: "karen" }
{
$rename:
{
"info.branch": "branch",
"balance": "info.balance"
}
}
)
- 普通重命名字段名
$rename 命令中的旧字段和新字段都不可以指向数组元素
$inc & $mul
更新字段值, $inc
加减字段值(正负数),$mul
相乘字段值,格式如下:
1 | { $inc: { <field1> : <value1>, ...}} |
举个例子 david
账户的 notYetExist
值加 10
:
1 | db.accounts.update( |
使用注意:
$inc
、$mul
只能应用在数字字段上- 当更新字段不存在时
$inc
会创建字段,并且将字段值设定为命令中的增减值$mul
会创建字段,但是把字段值设为0
$min & $max
比较后更新值,$min
比较后保留较小字段值 $max
比较后保留较大字段值:
1 | { $min: { <field1> : <value1>, ...}} |
举个例子:
会将 info.balance
的当前值同 5000
比较,保留较小的值。
1 | db.accounts.update( |
使用注意:
- 可以在任何可以比较大小的字段上使用
- 当更新字段不存在时
$min
和$max
都会创建字段,并且将字段值设定为命令中的更新值。
- 被更新的字段类型和更新值类型不一致
$min
和$max
会按照BSON
数据类型排序规则进行比较。不同
BSON
类型的值时,MongoDB
使用以下从低到高的比较顺序:
MinKey (internal type)
Null
Numbers (ints, longs, doubles, decimals)
Symbol, String
Object
Array
BinData
ObjectId
Boolean
Date
Timestamp
Regular Expression
MaxKey (internal type)
更新数组操作符
$addToSet
向数组中增添元素$pop
从数组中移除元素$pull
从数组中有选择性的移出元素$pullAll
从数组中有选择性的移出元素$push
向数组中增添元素
以上字段只能用到数组字段上,否则会收到错误
$addToSet
$addToSet
向数组中增添元素:
1 | { $addToSet: { <field1> : <value1>, ...}} |
- 如果要插入的值不存在数组字段中
- 新增字段会被添加到原文档中
- 如果要插入的值已经存在数组字段中
$addToSet
不会再添加重复值。 使用$addToSet
插入数组和文档时,插入值中的字段顺序和已有值重复时,才被算着重复值被忽略
$addToSet
会将数组插入被更新的数组字段中,成为内嵌数组。
1 | db.accounts.update( |
如果想要将多个元素直接添加到数组字段中,则需要使用 $each
操作符
1 | db.accounts.update( |
$pop
$pop
从数组字段中删除元素, 只能删除数组中的第一个(-1)和最后一个元素(1), 删除掉数组中最后一个元素后,会留下空数组。
1 | { $pop: { <field>: < -1 | 1 >, ...}} |
从 karen
的账户文档中删除最后一个联系方式:
1 | db.accounts.update( |
$pull
$pull
从数组中删除符合值或者条件的元素
1 | { $pull: { <field1>: <value|condition>, ... }} |
从 karen
的联系方式中删除包含 hi
字母的元素:
针对数组顶级元素的筛选无需使用 $elemMatch
, 下面是错误的:
1 | db.accounts.update( |
应该是:
1 | db.accounts.update( |
要删除数组元素是内嵌数组,可以使用 $elemMatch
对内嵌数组进行筛选:
从 karen
的联系方式中删除包含 22222222
的内嵌数组元素:
1 | db.accounts.update( |
$pullAll
1 | { $pullAll: { <field1>: [<value1, value2>, ...], ... }} |
其实
1 | { $pullAll: { <field1>: [<value1, value2>] }} |
相当于
1 | { $pull: { <field1>: {$in: [<value1, value2>] }}} |
如果要删除的元素是一个数组,数组的元素的值和排列顺序都必须和要被删除的数组完全一样。
举个例子:
1 | db.accounts.update( |
如果删除的元素是一个文档
$pullAll
命令只会删除字段和排列顺序都完全匹配的文档元素$pull
命令会删除匹配的文档元素,模糊度更高(形成包含关系,或者能通过字段名找到)。
现有文档格式:
1 | db.accounts.find() |
pullAll
不完全匹配无法删除:
1 | db.accounts.update( {name:"alice"}, {$pullAll: {contact:[{"scondaryEmail":"yyy@gmail.com"}]}}) |
pull
不完全匹配可以删除:
1 | db.account.update( {name:"alice"}, {$pull: {contact : {"secondaryEmail" : "yyy@gmail.com"}}}) |
$push
$push
向数组中添加元素
1 | { $push: { <field1> : <value1>, ...}} |
$push
和 addTosSet
命令相似地方:
如果要插入的值不存在数组字段中
- 新增字段会被添加到原文档中
可以搭配
$each
1
2
3
4
5
6
7
8db.accounts.update(
{name: "lawrence"},
{ $push: {
newArray: {
$each:[2, 3, 4]
}
}}
)$push
比addTosSet
增强地方:搭配
$each
实现更复杂的操作使用
$position
操作符将元素插入到数组的指定位置,$position
的值0
表示 从数组第一个位置开始插入,-2
表示从数组最后一个元素往前走2
个开始插入。1
2
3
4
5
6
7
8
9db.accounts.update(
{name: "lawrence"},
{ $push: {
newArray: {
$each:["pos1", "pos2"],
$position: 0
}
}}
)使用
$sort
排序1
2
3
4
5
6
7
8
9db.accounts.update(
{name: "lawrence"},
{ $push: {
newArray: {
$each:["sort1"],
$sort: 1
}
}}
)如果插入的元素是内嵌文档,也可以根据内嵌文档的字段值排序
1
2
3
4
5
6
7
8
9db.accounts.update(
{name: "lawrence"},
{ $push: {
newArray: {
$each:[{key: "sort", value: 100}, {key: "sort1", value: 200}],
$sort: { value: -1 } // 只排序插入的内嵌文档
}
}}
)不想插入,只想对数组中的字段进行排序:
1
2
3
4
5
6
7
8
9db.accounts.update(
{name: "lawrence"},
{ $push: {
newArray: {
$each:[],
$sort: -1
}
}}
)使用
$slice
截取部分数组插入
slice1
元素后,保留从后往前数8
个元素:1
2
3
4
5
6
7
8
9db.accounts.update(
{name: "lawrence"},
{ $push: {
newArray: {
$each:["slice1"],
$slice: -8
}
}}
)不想插入,只想截取部分数组:
保留数组中的前
6
个元素:1
2
3
4
5
6
7
8
9db.accounts.update(
{name: "lawrence"},
{ $push: {
newArray: {
$each:[],
$slice: 6
}
}}
)
$position
, $sort
, $slice
可以一起使用, 他们的执行顺序是 $position > $sort > $slice
。写在命令中的操作顺序并不重要,并不会影响命令的执行顺序。
1 | db.accounts.update( |
$
占位符
$
是数组中第一个符合筛选条件的数组元素的占位符, 搭配更新操作符使用,可以对满足筛选条件的数组元素进行更新。
1 | db.collection.update( |
举个例子,将 lawrence
中 newArray
中的 pos2
替换为 updated
:
1 | db.accounts.update( |
$[]
占位符
$[]
指代数组中的所有元素
1 | { <update operator> : { "<array>.$[]": value }} |
举个例子,将 lawrence
账户中的 contact
数组中的第一个内嵌数组,全部替换为 999999
:
1 | db.accounts.update( |
options
multi
1 | { multi: <boolean> } |
默认情况下,即使筛选条件对应了多篇文档,update
命令仍然只会更新一篇文档
设置 multi
为 true
选项可以更新所有符合筛选条件的文档, 注意,MongoDB
只能保证单个文档操作的原子性,不能保证多个文档操作的原子性。多个文档操作时的原子性需要 MongoDB 4.0
版本引入的事务功能进行操作。
举个例子:
1 | db.accounts.update( |
upsert
1 | { upsert: <boolean> } |
更新或者创建文档
在默认情况下,如果 update
命令中的筛选条件没有匹配任何文档,则不会进行任何操作
将 upsert
设置为 true
, 如果 update
命令中的筛选条件没有匹配任何文档,则会创建新文档
举个例子:
1 | db.accounts.update( |
如果筛选条件中能推断出确定的字段,新创建的文档会包含筛选条件设计的字段
save
1 | db.<collection>.save(<document>) |
如果 document
文档中包含 _id
字段, save()
命令将会调用 db.collection.update()
命令(upsert: true)
, _id
值存在就更新,不存在就创建新文档。
举个例子:
1 | db.accounts.save( |
删除
- 删除集合
- 删除特定文档
删除文档
1 | db.<collection>.remove(<query>, <options>) |
<query>
筛选条件<options>
设置参数在默认情况下,
remove
命令会删除所有复合筛选条件的文档如果只想删除复合筛选条件的第一篇文档,可以使用
justOne
选项1
2
3
4db.accounts.remove(
{balance: { $lt: 100} },
{justOne: true }
)删除集合内的所有文档
1
db.accounts.remove({})
举个例子, 删除 balance
等 50
的文档:
1 | db.accounts.remove( |
删除集合
remove
只会删除集合内所有的文档,但不会删除集合
drop
命令可以删除整个集合,包括集合中的所有文档,以及集合的索引
1 | db.<collection>.drop({ writeConcern: <document> }) |
writeConcern
定义了本次集合删除操作的安全写级别
举个例子:
db.accounts.drop()
如果集合中的文档数量较多,使用
remove
命令删除所有的文档效率不高,这种情况下推荐,使用drop
命令删除集合,然后再创建空集合并创建索引。