二、MongoDB渐悉
下次再次从CRUD
操作开始,对MongoDB
的命令建立全面认知,先来看看新增/插入操作。
2.1、新增操作
MongoDB
中提供了三个显式向集合插入数据的方法:
// 向集合插入单条或多条数据(需要用[]来包裹多个文档)
db.xiong_mao.insert([
{_id:4,name:"黑熊",age:3,food:{name:"黄金竹",grade:"S"}},
{_id:5,name:"白熊",age:4,food:{name:"翠绿竹",grade:"B"}}
]);
// 向集合插入单条数据
db.xiong_mao.insertOne({_id:6,name:"棕熊"});
// 向集合批量插入多条数据
db.xiong_mao.insertMany([
{_id:7,name:"红熊",age:2,food:{name:"白玉竹",grade:"S"}},
{_id:8,name:"粉熊",age:6,food:{name:"翡翠竹",grade:"A"}}
]);
从案例中可发现,每个字段可以无限嵌套json
。同时,这三个insert
方法都有两个可选项,如下:
db.collection.insertXXX(
[<document 1>,<document 2>,...],
{
writeConcern:<document>,
ordered:<boolean>
}
)
writeConcern
表示嵌套文档,这个咱们后续再细说;ordered
表示本次是否按顺序插入,2.6
以上版本默认为true
,表示按指定的顺序插入数据。
除开上面三个常用的插入方法外,MongoDB
还可以通过其他方式实现数据新增,如:
const bulkOps =[
{insertOne:{document:{_id:9,name:"黄熊"}}},
{insertOne:{document:{_id:10,name:"灰熊"}}}
];
db.xiong_mao.bulkWrite(bulkOps);
bulkWrite()
方法可以传入一个操作数组,MongoDB
会按照给定的操作顺序依次执行,其中还可以放修改、删除等MongoDB
支持的操作。最后,还能通过修改命令,向集合中隐式插入数据,在使用修改命令时,将upsert
设为true
即可(后面会演示)。
2.2、删除操作
MongoDB
中的删除命令,有remove、delete
两类方法:
// 根据指定条件删除集合中的数据
db.xiong_mao.remove({_id:1});
// 删除集合中的所有数据(不写条件即可)
db.xiong_mao.remove({});
// 根据条件删除单条数据(有多条数据满足条件时,只会删第一条)
db.xiong_mao.deleteOne({_id:6, name:"棕熊"});
// 根据条件删除多条数据(满足条件的全部删除)
db.xiong_mao.deleteMany({name:"红熊"});
// 根据条件删除满足条件的第一条数据,并返回删除后的数据
db.xiong_mao.findOneAndDelete({_id:2});
当然,这里只列出了根据等值条件删除的操作,类似于SQL
中的<、>、in、or、and……
怎么写呢?这些放到后续的查询操作中讲解,因为条件过滤器在MongoDB
中是通用的。
2.3、修改操作
想要更新一个集合中的数据时,可以使用update、replace
操作:
// 根据条件修改集合中的单条数据(多条数据满足条件时,只修改第一条)
db.xiong_mao.updateOne({_id:7},{$set:{name:"英熊"}});
// 根据条件修改集合中的多条数据(color字段不存在时,自动创建并赋值)
db.xiong_mao.updateMany({age:3},{$set:{color:"黑白色"}});
// 根据条件替换掉单行数据(使用新数据替换老的数据)
db.xiong_mao.replaceOne({_id:6},{name:"棕熊",age:3,color:"棕色"});
// 根据条件修改集合中的单条数据(可以通过.分割的形式,修改嵌套的json)
db.xiong_mao.update({_id:8},{$set:{age:3,"food.grade":"C"}});
// 根据条件修改集合中的单/多条数据(如果想通过update更新多条,需要使用multi)
db.xiong_mao.update({age:3},{$set:{hobby:"竹子"}},{multi:true});
修改操作默认是全量修改,即使用指定的值,覆盖原有的整条数据,如果有些字段值在修改时未给定,则会变为null
(消失)。为此,如若只想修改某几个字段,记得在前面加上$set
选项。
当然,这里也仅是用_id
在做等值条件修改,后面讲完查询操作后,大家可以自行套入各种复杂条件进行修改。
还记得提过的“修改时隐式新增”嘛?演示一下:
db.xiong_mao.update(
{_id:11},
{name:"紫熊", age:3, food:{name:"明月竹", grade:"A"}},
{upsert:true}
);
在修改时,只需指定upsert:true
即可,这样在未匹配到对应条件的数据时,会将本次修改的数据插入到集合中,而前面演示的所有修改方法,都支持指定upsert
选项。
修改操作除开上述方法外,还可以通过findAndModify()、findOneAndUpdate()、findOneAndReplace()
方法来修改,如:
db.xiong_mao.findAndModify({
query:{_id:8},
update:{$set:{color: "紫色"}},
new: true
});
这些findAnd
开头的修改方法,会根据条件修改满足条件的第一条数据,修改完之后,如果new
指定为true
,则会返回修改后的数据,如果为false
,则返回修改前的数据(其余两个方法类似,不再继续演示)。
2.4、查询操作
增删改查的前三项过完了,接着来说说查询操作,这应该属于最复杂的一项,咱们一点点开始接触。
2.4.1、基本查询语句
// 查询集合所有数据
db.xiong_mao.find();
// 根据单个等值条件查询数据
db.xiong_mao.find({_id:2});
// 查询满足多个等值条件的数据(and查询)
db.xiong_mao.find({age:3,"food.grade":"S"});
// 查询满足任意一个条件的数据(or查询)
db.xiong_mao.find({$or:[{age:2},{"food.grade":"S"}]});
// 查询单字段满足任意一个条件的数据(in查询)
db.xiong_mao.find({age:{$in:[2,4]}});
// 查询颜色为黑白色,并且年龄小于5岁的数据
db.xiong_mao.find({color:"黑白色",age:{$lt:5}});
// 查询爱好为竹子,或id大于9的数据
db.xiong_mao.find({$or:[{hobby:"竹子"},{_id:{$gt:9}}]});
// 查询id小于等于5,并且(age大于等于2 或 name为肥肥)的数据
db.xiong_mao.find({
_id:{$lte:5},
$or:[{age:{$gte:2}},{name:"肥肥"}]
});
// 查询name不为“白熊” 或 age 不大于 2 的数据
db.xiong_mao.find({$nor:[{name:"白熊"},{age:{$gt:2}}]});
// 查询id在3~5之间的数据(between and范围查询)
db.xiong_mao.find({$and:[{_id:{$gte:3}},{_id:{$lte:5}}]});
// 查询name以“粉”开头的数据(like右模糊查询)
db.xiong_mao.find({name:/^粉/});
上述一些查询语句,对应着SQL
中的基本查询,如=、<、>、<=、>=、in、like、and、or
,大家仔细观察会发现,其中有许多$
开头的东东,这个是啥?在MongoDB
中称之为操作符,操作符有许多,分别对应着SQL
中的关键字与特殊字符,下面列写常用的:
SQL | MongoDB |
---|---|
= |
: |
=、<、>、<=、>=、!= |
$eq、$lt、$gt、$lte、$gte、$ne |
in、not in |
$in、$nin |
and、or、not、is null |
$and、$or、$not、$exists |
+、-、*、/、% |
$add、$subtract、$multiply、$divide、$mod |
group by、order by |
$group、$sort |
…… |
…… |
上表仅仅只列出了一些在SQL
中比较常见的,实则MongoDB
提供了几百个操作符,以此来满足各类场景下的需求,如果你想要详细了解,可以参考MongoDB官网-Operators,当然,如果你英语阅读能力欠佳,可以参考MongoDB中文网-运算符。
接着来看看其他查询的语法:
// 对指定字段去重,并返回去重后的字段值列表
db.xiong_mao.distinct("age");
// 根据条件统计集合内的数据行数
db.xiong_mao.count({age:3});
// 根据条件进行分页查询(limit写行数,skip写跳过前面多少条)
db.xiong_mao.find({_id:{$lt:6}}).skip(0).limit(2);// 第一页
db.xiong_mao.find({_id:{$lt:6}}).skip(2).limit(2);// 第二页
// 根据指定字段进行排序查询(order by查询)
// 根据年龄升序,年龄相同根据id降序(1:升序,-1:降序)
db.xiong_mao.find().sort({age:1,_id:-1});
// 投影查询:只返回指定的字段(0表示不返回,1代表返回)
db.xiong_mao.find({color:"黑白色"},{_id:0,name:1,color:1});
这里又列出了一些SQL
中经常执行的操作,如统计、去重、排序、分页、投影查询等,和SQL
一样,不同类型的函数,执行的优先级不一样,例如:
db.xiong_mao.find({_id:{$lt:6}}).limit(2).skip(2).sort({_id:-1}).count();
这条查询语句中,包含了limilt()、skip()、sort()、count()
四个方法,可实际的执行顺序,跟书写的顺序无关,这些方法同时存在时,执行的优先级为:sort() > skip() > limilt() > count()
。
2.4.2、聚合管道查询
好了,如果你想要实现更复杂的查询操作,则可以通过MongoDB
提供的聚合管道来完成,语法如下:
db.collection.aggregate(
pipeline:[<stage>,<...>],
options:{
explain:<boolean>,
allowDiskUse:<boolean>,
cursor:<document>,
maxTimeMS:<int>,
bypassDocumentValidation:<boolean>,
readConcern:<document>,
collation:<document>,
hint:<stringor document>,
comment:<any>,
writeConcern:<document>,
let:<document>// Added in MongoDB 5.0
}
);
聚合管道方法接收两个入参,第一个是数组型的聚合操作,第二个是可选项,先解释下常用的选项:
explain
:传true
表示返回聚合管道的详细执行计划;allowDiskUse
:是否允许使用硬盘进行聚合操作,内存不足时使用磁盘临时文件进行计算;maxTimeMS
:指定聚合操作的最大执行时间(单位ms
),超时会被强制终止;hint
:显式指定使用那些索引进行聚合操作;
简单了解几个常用选项后,我们来重点关注一下pipeline
参数,使用方式如下:
db.collection.aggregate([
// 阶段1
{$stage1:{/* 阶段1的操作 */}},
// 阶段2
{$stage2:{/* 阶段2的操作 */}},
// ...
]);
观察上述语法,首先咱们需要指定操作符,这是为了声明当前阶段的类型,例如$group
,接着可以指定每个阶段具体要做的事情。同时,聚合管道中的每个阶段,都会将上一阶段输出的数据,视为当前阶段输入的数据,和Stream
流类似,下面上些例子理解,如下:
/* 按年龄进行分组,并统计各组的数量(没有age字段的数据统计到一组) */
db.xiong_mao.aggregate([
// 1:通过$group基于age分组,通过$sum实现对各组+1的操作
{$group:{_id:"$age",count:{$sum:1}}},
// 2:基于前面的_id(原age字段)进行排序,1代表正序
{$sort:{_id:1}}
]);
/* 按年龄进行分组,并得出每组最大的_id值 */
db.xiong_mao.aggregate([
// 1:先基于age字段分组,并通过$max得到最大的id,存到max_id字段中
{$group:{_id:"$age",max_id:{$max:"$_id"}}},
// 2:按照前面的_id(原age字段)进行排序,-1代表倒序
{$sort:{_id:-1}}
]);
/* 过滤掉食物为空的数据,并按食物等级分组,返回每组_id最大的熊猫姓名 */
db.xiong_mao.aggregate([
// 1:通过$match操作符过滤food不存在的数据
{$match:{food:{$exists:true}}},
// 2:通过$sort操作符,基于_id字段进行倒排序
{$sort:{_id:-1}},
// 3:通过$group基于食物等级分组,并通过$max得到_id最大的数据,
// 并通过$first拿到分组后第一条数据(_id最大)的name值
{$group:{_id:"$food.grade",max_id:{$max:"$_id"},name:{$first:"$name"}}},
// 4:最后通过$project操作符,只显示_id(原food.grade)、name字段
{$project:{_id:"$_id",name:1}}
]);
上面举了三个简单的聚合操作例子,但相信对于绝大多数刚接触的小伙伴来说,这些语句看起来十分头大,一眼望过去全都是符号+符号,这是因为MongoDB
中,使用操作符代替了SQL
中的函数与关键字,所以越是复杂的场景,使用到的操作符越多,对传统型的SQL Boy
来说,可读性会越差~
这里为了便于大家理解,先列出MongoDB
聚合管道操作符和SQL
聚合关键字/函数的对比:
SQL关键字/函数 | MongoDB聚合操作符 |
---|---|
where、having |
$match |
group by |
$group |
order by |
$sort |
select field1、field2… |
$project |
limit |
$limit、$skip |
sum()、count()、avg()、max()、min() |
$sum、$sum:1、$avg、$max、$min |
join |
$lookup |
当然,上面列出的仅是沧海一粟,MongoDB
的聚合管道中可用的操作符有几百个,不管是SQL
中有的,还是没有的功能/函数,都能找到对应的操作符代替,具体大家可以参考MongoDB官网-Aggregation Pipeline Operators,或MongoDB中文网-聚合管道操作符。
话归正题,咱们来稍微解读一下前面写出的聚合管道查询语句,先拿最简单的第一条来说:
/* 按年龄进行分组,并统计各组的数量 */
db.xiong_mao.aggregate([
{$group: {_id:"$age", count: {$sum:1}}},
{$sort: {_id:1}}
]);
在第一阶段中,使用$group
操作符来完成分组动作,其中_id
并不是原数据中的_id
,而是代表想聚合的数据主键,上面的需求想基于年龄分组,所以_id
设置为$age
即可。
注意:在聚合查询中,也会使用
$
来引用原数据中的字段,案例中的$age
并不是一个操作符,而是获取原数据的age
字段,这种方式也可以用来拿其他字段,如$name、$color……
。
在第一阶段中,咱们还定义了一个count
字段,这相当于SQL
中的别名,该字段的值,则是由{$sum:1}
统计得出的,这段逻辑相当于:
Map<Integer,Integer> groupMap =newHashMap<>();
for(XiongMao xm : xiongMaoList){
Integerage= xm.getAge();
if(groupMap.get(age)==null){
groupMap.put(age,1);
}else{
Integercount= groupMap.get(age)+1;
groupMap.put(age, count);
}
}
第一阶段的分组统计工作完成后,接着会来到第二阶段,其中使用了$sort
操作符,对_id
字段进行了升序排序,不过这里要注意,因为第二阶段输入的数据,是第一阶段输出的数据,所以第二阶段的_id
,实际上是原本的age
字段,这意味着在对年龄做升序排序!
OK,相信经过这番解释后,大家对MongoDB
的聚合管道操作有了一定理解,那接着继续看些案例加深印象(也可以尝试着自己练习练习):
/* 多字段分组:按食物等级、颜色字段分组,并求出每组的年龄总和 */
db.xiong_mao.aggregate([
// 1:_id中写多个字段,代表按多字段分组,接着通过$sum求和age字段
{$group:{_id:{grade:"$food.grade",color:"$color"},total_age:{$sum:"$age"}}}
]);
/* 分组后过滤:根据年龄分组,然后过滤掉数量小于3的组 */
db.xiong_mao.aggregate([
// 1:先按年龄进行分组,并通过$sum:1对每组数量进行统计
{$group:{_id:"$age",count:{$sum:1}}},
// 2:通过$match操作符,保留数量>3的分组(过滤掉<3的分组)
{$match:{count:{$gt:3}}}
]);
/* 分组计算:根据颜色分组,求出每组的数量、最大/最小/平均年龄、所有姓名、首/尾的姓名 */
db.xiong_mao.aggregate([
// 1:按颜色分组
{$group:{_id:"$color",
// 计算每组数量
count:{$sum:1},
// 计算每组最大年龄
max_age:{$max:"$age"},
// 计算每组最小年龄
min_age:{$min:"$age"},
// 计算每组平均年龄
avg_age:{$avg:"$age"},
// 通过$push把每组的姓名放入到集合中
names:{$push:"$name"},
// 获取每组第一个熊猫的姓名
first_name:{$first:"$name"},
// 获取每组最后一个熊猫的姓名
last_name:{$last:"$name"}}}
]);
/* 分组后保留原数据,并基于原_id排序,然后跳过前3条数据,截取5条数据 */
db.xiong_mao.aggregate([
// 1:先基于color分组,并通过$$ROOT引用原数据,将其保存到数组中
{$group:{_id:"$color",xiong_mao_list:{$push:"$$ROOT"}}},
// 2:通过$unwind操作符,将xiong_mao_list数组分解成一条条数据
{$unwind:{path:'$xiong_mao_list',
// 使用index字段记录数组下标,preserveNullAndEmptyArrays可以保证不丢失数据
includeArrayIndex:"index",preserveNullAndEmptyArrays:true}},
// 3:基于分解后的_id字段进行排序,1代表升序
{$sort:{"xiong_mao_list._id":1}},
// 4:通过$skip跳过前3条数据
{$skip:3},
// 5:通过$limit获取5条数据
{$limit:5}
]);
/* 根据年龄进行判断,大于3岁显示成年、否则显示未成年(输出姓名、结果) */
db.xiong_mao.aggregate([
// 1:通过$project操作符来完成投影输出
{$project:{
// 不显示_id字段,将name字段重命名为:“姓名”
_id:0,姓名:"$name",
// 通过$cond实现逻辑运算,如果年龄>=3,显示成年,否则显示未成年
result:{
$cond:{
if:{$gte:["$age",3]},
then:"成年",
else:"未成年"
}}}}
]);
好了,关于聚合管道的案例,暂时就写到这里,想要学习其他操作符的使用,可以参考之前给出的官网链接。这里主要说明几点要素。
首先聚合管道的每个阶段,内存限制一般为100MB
,如果超出这个限制,执行时会报错,因此当需要处理大型数据集时,记得把allowDiskUse
选项置为true
,允许MongoDB
使用磁盘临时文件来完成计算。
其次呢,使用聚合管道时也要牢记,筛选、过滤类型的阶段,最好放到前面完成,这样有两个好处:一是可以快速将不需要的文档过滤掉,减少管道后续阶段的工作量;二是如果在$project、$group
这类操作前执行$match
,$match
阶段可以使用索引,因为此时属于在操作原本的文档,索引自然不会失效,可如果放到$project、$group……
后面再执行,原数据被改变后,前面的阶段会形成新的文档,并输出到$match
阶段中,这时索引不一定能继续生效。
最后,按官网所说,除了$out、$merge
和$geoNear
这些阶段外,其他类型的所有阶段,都可以在管道中出现多次,这意味着MongoDB
会比传统型SQL
更强大,比如可以多次分组、筛选……,结合管道里的几百个操作符,你可以实现各种复杂场景下的聚合操作。
2.4.3、嵌套文档的增删改查
在关系型数据库中,我们可以通过树表、主子表等思想,抽象出各种结构以满足业务需求,举个例子理解,比如下单业务中,用户一笔订单可能会包含多个商品,所以会用主子表的思想,对订单数据进行存储,订单数据存到主表中、订单详情数据放到子表,两表之间通过主外键关联,后续可以通过join
来查询、使用。
但由于MongoDB
是无模式、半结构化的存储方式,因此无法像传统型数据库那样,通过主外键来关联不同表(集合),毕竟一个集合没有明确的字段定义,可以随意插入不同字段。那MongoDB
又该如何满足树表、主子表这种业务的存储需求呢?答案是通过文档嵌套来代替!
在前面学习insert
操作时,大家不难得知:MongoDB
集合中的每个字段,可以继续嵌套Json
对象,因为MongoDB
本身就是以Json
格式存储数据,所以只要是能用Json
格式表达出来的数据,都可以放到集合中存储,比如:
db.xiong_mao.insert([
{_id:12, name:"金熊", age:4, hobby:["吃饭","睡觉"], food:{name:"黄金竹", grade:"S"}}
]);
在插入数据时,字段的值可以放入普通类型,也可以放入数组、另一个Json
对象,所以早期的MongoDB
,推荐通过嵌入文档,来代替SQL
中的join
多表连接查询,前面举例提到的订单、订单详情这种一对多的主子关系,可以用如下方式代替:
/* 以下数据仅为示例,不要纠结订单数据的不完整性 */
db.order.insert([{
_id:1,
order_number:"x-20230811185422",
order_status:"已付款",
pay_fee:888.88,
pay_type:"他人代付",
order_user_id:"2222222",
pay_user_id:"11111111",
pay_time:newDate(),
order_details:[
{order_detail_id:1,shop_id:6,shop_price:666.66,status:"待发货"},
{order_detail_id:2,shop_id:2,shop_price:222.22,status:"待发货"}
]
}]);
这样,当需要查询一笔订单的详情记录时,就直接查询oder
集合即可,因为订单详情数据存储在order_details
字段中,这个是一个数组,每个数组的元素则是一条条Json
格式的订单详情。
下面来聊聊嵌套文档的增删改查,这里跟操作普通文档有些许出入,如下:
/* 新增单条订单详情:通过order集合的update方法来实现 */
db.order.update(
// 修改条件:修改_id=1的订单数据
{_id:1},
// 通过$push操作符,往order_details中压入一条新记录
{$push:{
order_details:{
order_detail_id:3,
shop_id:4,
shop_price:444.44,
status:"待发货"
}
}}
);
/* 新增多条订单详情记录 */
db.order.update(
{_id:1},
{$push:{
order_details:{
// 这里使用$each操作符,声明本次要push多个元素
$each:[
{order_detail_id:4,shop_id:3,shop_price:333.33,status:"待发货"},
{order_detail_id:5,shop_id:1,shop_price:111.11,status:"待发货"}
]
}
}}
);
/* 删除满足条件的订单详情数据 */
db.order.update(
{_id:1},
// 通过$pull操作符,将order_details满足条件的数据弹出
{$pull:{
// 删除order_detail_id大于3的订单详情数据
order_details:{order_detail_id:{$gt:3}}
}
});
/* 修改单条满足条件的订单详情数据 */
db.order.update(
// 这里给定了两个条件,订单ID=1,并且订单详情ID=1
{_id:1,"order_details.order_detail_id":1},
// 通过$set操作符,来对集合中的文档进行局部修改
{$set:{
// 将状态改为已发货,这里的$,表示数组中的当前元素,即条件匹配的数组元素
"order_details.$.status":"已发货"
}}
);
/* 修改多条满足条件的订单详情数据 */
db.order.update(
// 先查询到订单ID=1的数据
{_id:1},
{$set:{
// 这里使用的是$[elem]占位符,代表着要更新的元素
"order_details.$[elem].status":"已发货"
}},
// 这里定义了一个数组过滤器,会把满足条件的订单详情记录筛选出来
{arrayFilters:[
// 这里的elem代表数组中的元素,过滤条件为:订单详情ID=2、3的数据
{"elem.order_detail_id":{$in:[2,3]}}
]}
);
/* 修改满足多个条件的单条数据 */
db.order.update(
{
// 这里是基于order_details字段在做条件过滤,可以跨文档(在不同数据行中筛选)
order_details:{
// $elemMatch操作符代表匹配多个条件
$elemMatch:{
status:"已发货",
shop_id:2
}}
},
// 对满足多个条件的单条数据进行修改(要修改多条,参考上个案例的用法即可)
{$set:{
"order_details.$.status":"已签收"
}}
);
/* 查询符合条件的单条数据 */
db.order.find(
// 查询order_detail_id==1的订单详情数据
{"order_details.order_detail_id":1},
// 限制返回的字段,不返回_id,order_details.$表示只返回满足条件的订单详情数据
{"_id":0,"order_details.$":1}
);
/* 查询满足多个条件的多条数据 */
db.order.aggregate([
// 使用$project操作符完成投影查询
{$project:{
// 不返回_id字段
_id:0,
// 只返回order_details字段
order_details:{
// 通过$filter操作符,实现按条件过滤数据
$filter:{
// 这里相当于for循环,$order_details是要遍历的数组,$detail是别名
input:"$order_details",
as:"detail",
// 这里是过滤的条件,$and表示两个条件都需要满足
cond:{
$and:[
// 返回order_detail_id>=1 && status为“已签收”的数据
{$gte:["$$detail.order_detail_id",1]},
{$eq:["$$detail.status","已签收"]}
]
}
}
}}}
]);
好了,上面把嵌套文档的增删改查操作简单过了一遍,其实嵌入式文档的CRUD
,都依赖于外部集合的CRUD
方法完成,唯一有区别的地方就在于:我们需要结合$push、$pull、$elemMatch、$[elem]
等各种数组操作符,来实现嵌入式文档的增删改查。不过如若只嵌入了一个文档,而不是一个文档数组时,操作的方法有有些不同,如下:
// 以之前xiong_mao集合中,id=1的这条数据为例
db.xiong_mao.insert({_id:1,name:"肥肥",age:3,hobby:"竹子"});
// 为其添加一个内嵌文档
db.xiong_mao.update(
{_id:1},
{$set:{food:{name:"帝王竹",grade:"A"}}},
{upsert:true}
);
// 查询一次看看这条数据(此时可以看到内嵌文档已经被添加)
db.xiong_mao.find({_id:1});
{
_id:1,
name:'肥肥',
age:3,
hobby:'竹子',
color:'黑白色',
food:{name:'帝王竹',grade:'A'}
}
// 修改内嵌文档中的某个字段值
db.xiong_mao.updateMany(
{_id:1},
// 修改单个字段,要用 . 的形式
{$set:{"food.grade":"S"}},
{upsert:true}
);
// 删除内嵌文档中的单个字段
db.xiong_mao.update(
{_id:1},
// 通过$unset操作符,将对应字段置为空即可
{$unset:{"food.grade":""}},
{upsert:true}
);
// 删除整个内嵌文档
db.xiong_mao.update(
{_id:1},
// 将整个内嵌文档字段置为空即可
{$unset:{food:""}},
{upsert:true}
);
ok,关于文档嵌入的内容咱们先就此打住,来思考一个问题:真的所有带有主外键关系的结构,都可以通过文档嵌入代替吗?
这个答案明显是NO
,为什么?就拿咱们前面“订单-订单详情”例子中的商品来说,每一笔订单详情记录,都需要跟商品产生绑定关系吧?这时我们总不能把整个商品的数据,全部嵌入到订单详情的某个字段中吧?因为一个商品可以被多次购买!如果真按这样存,假设一个商品单月销量10W+
,产生的10W+
订单详情记录,难道都把这个商品的完整数据嵌入一次吗?显然不合理。
正因如此,有些场景下,咱们无法通过文档嵌入来代替原本的多表关联,所以MongoDB
在3.2
版本以后,也支持了多个集合之间关联查询,就类似于SQL
中的join
一样!只不过功能没SQL
强大,并且必须在聚合管道中使用,下面一起来看看。
2.4.4、多个集合关联查询
现在假设有商店shop_store
、商品product
两个集合,数据如下:
db.shop_store.insertMany([
{_id:1,name:"熊猫高级会所",address:"地球市天上人间街道888号",grade:"五星"},
{_id:2,name:"竹子商店",address:"地球市绿竹林街道666号",grade:"五星"}
]);
db.product.insertMany([
{_id:1,shop_store_id:1,name:"水",description:"能有效缓解口渴",price:2.00},
{_id:2,shop_store_id:2,name:"米饭",description:"能有效缓解饥饿",price:1.00},
{_id:3,shop_store_id:1,name:"香烟",description:"能有效缓解寂寞",price:88.88},
{_id:4,shop_store_id:1,name:"酒",description:"能有效缓解忧愁",price:99.00},
{_id:5,shop_store_id:1,name:"牛奶",description:"能帮助长身体",price:4.00},
{_id:6,shop_store_id:2,name:"咖啡",description:"能有效缓解疲劳",price:9.99},
{_id:7,shop_store_id:1,name:"棉衣",description:"能有效缓解寒冷",price:188.00},
{_id:8,shop_store_id:2,name:"辣条",description:"能有效缓解嘴馋",price:5.00}
]);
在以外,为了实现数据的关联性,我们会直接将商店数据,嵌入到商品数据的某个字段中,而在目前的这个例子中,却是通过shop_store_id
字段来关联商品所在的商店,查询时又该如何处理呢?如下:
db.shop_store.aggregate([
{
$lookup:{
from:"product",
localField:"_id",
foreignField:"shop_store_id",
as:"products"
}
}
]);
在上述代码中,使用了聚合管道中的$lookup
操作符,来实现多个集合之间的关联查询,其中主要有四个参数:
from
:要关联的集合名称,这里是用商店关联商品,所以写product
;localField
:shop_store
集合中用于关联的字段,这里是_id
;foreignField
:product
集合中用于关联的字段,这里是shop_store_id
;as
:指定两个集合匹配的数据,要保存到的字段名称,这里写了products
。
由于关联查询,也依靠于聚合管道来完成,为此大家可以在关联查询的前后,插入各种业务所需的阶段,以满足各种特殊场景下的需求,不过这里注意:关联查询结束后,驱动集(调用aggregate
方法的集合)的字段会完整输出,而被驱动集的字段,会以Json
对象的形式,放入到指定的字段中,最终形成一个Json
数组,即案例中的products
。
最后,如果你的需求中,需要关联并筛选数据,那么最好将筛选动作放到关联查询前面,尽可能的减小关联查询时的数据集,因为$lookup
操作符,底层就是两个for
循环,如果两个集合都有100W
条数据,未做筛选直接关联查询,此时性能堪忧……
2.5、数据类型
关于CRUD
操作相关的命令咱们就此打住,掌握上述那些基本够用了,下面来看看MongoDB
中的数据类型。
看完前几个阶段的内容大家会发现,MongoDB
的命令,用的其实就是JavaScript
的语法,所以在数据类型这块,原生JS
中支持的MongoDB
几乎都支持,这里就简单列一下:
• String
:存储变长的字符串;
• Number
:存储整数或浮点型小数;
• Boolean
:存储true、flase
两个布尔值;
• Date
:存储日期和时间;
• Array
:可以存储由任意类型组成的数组;
• Embedded-Document
:可以将其他结构的文档,嵌套到一个文档的字段中;
• Binary
:存储由0、1
组成的二进制数据,如图像、音/视频等;
• Code
:MongoDB
支持直接存储JavaScript
代码;
• Timestamp
:存储格林威治时间(GMT
)的时间戳;
• GeoJSON
:存储地理空间数据;
• Text
:存储大文本数据;
从上面列出的类型不难发现,其实这些数据,的的确确都是原生JS
支持的类型~
2.6、事务与锁机制
MongoDB
作为数据库家族的一员,自然也支持事务机制,只不过相较于InnoDB
的事务机制而言,MongoDB
事务方面并没有那么强大,这倒不是因为官方技术欠缺,而是由于MongoDB
的定位是:大数据、高拓展、高可用、分布式,因此在实现事务时,不仅仅要考虑单机事务,而且需要考虑分布式事务,复杂度上来之后,自然无法做到MySQL-InnoDB
那种单机事务的强大性。
这里也列出MongoDB
事务方面的改进过程,如下:
3.0
版本中,引入WiredTiger
存储引擎,开始支持单文档事务;4.0
版本中,开始支持多文档事务,以及副本集(主从复制)架构下的事务;4.2
版本中,开始支持分片集群、分片式多副本集架构下的事务。
再次类比MySQL
,MongoDB
支持了分片集群中的事务,而MySQL
只支持主从集群下的事务,并不支持分库环境下的事务,在这方面,MongoDB
强于MySQL
,下面来看看事务怎么使用:
// 开启一个会话
var session = db.getMongo().startSession({readPreference:{mode:"primary"}});
// 开启事务:指定读模式为快照读,写模式为半同步,即写入半数以上节点后再返回成功
session.startTransaction({readConcern:{level:"snapshot"},writeConcern:{w:"majority"}});
// 获取要操作的集合对象
var trx_coll = session.getDatabase("库名").getCollection("集合名");
// 要在事务里执行的CRUD操作
......
// 回滚事务命令
session.abortTransaction();
// 提交事务命令
session.commitTransaction();
// 关闭会话命令
session.endSession();
大家一眼看下来,会发现使用起来非常麻烦,首先需要先拿到一个会话,接着开启事务时,还要指定一堆参数,回滚/提交事务的命令也不一样……,为此,如若之前没接触过的小伙伴,看着或许会很别扭。
这里重点解释一下开启事务的startTransaction()
方法的参数,以及可选项,如下:
-
readConcern
:指定事务的读取模式 -
local
:读取最近的数据,可能包含未提交的事务更改; -available
:读取已提交的数据,可能包含尚未持久化的事务更改; -snapshot
:读取事务开始时的一致快照,不包含未提交的事务更改; -level
:指定一致性级别 -
writeConcern
:指定事务的写入模式 -
[number]
:写操作在写入指定数量的节点后,返回写入成功; -majority
:写操作在写入大多数节点(半数以上)后,返回写入成功; -tagSetName
:写操作在写入指定标签的节点后,返回写入成功; -w
:指定写操作的确认级别(同步模式) -j
:写入是否应被持久化到磁盘; -wtimeout
:指定写入确认的超时时间; -
readPreference
:定义读操作的节点优先级和模式 -
primary
:只从主节点读取数据; -secondary
:只在从节点上读取数据; -primaryPreferred
:优先从主节点读取,主节点不可用,转到从节点读取; -secondaryPreferred
:优先在从节点读取,从节点不可用,转到主节点读取; -nearest
:从可用节点中选择最近的节点进行读取; -mode
:指定读取模式 -tagSets
:在带有指定的标签的节点上执行读取操作
OK,对于MongoDB
的事务机制,了解上述命令即可,毕竟MongoDB
本身就不适用于强事务的场景,原因如下:
- ①
MongoDB
的事务必须在60s
内完成,超时将自动取消(因为要考虑分布式环境); - ②涉及到事务的分片集群中,不能有仲裁节点(后面会解释仲裁节点);
- ③事务会影响集群数据同步效率、节点数据迁移效率;
- ④多文档事务的所有操作,必须在主节点上完成,包括读操作;
综上所述,就算MongoDB
支持事务,可实际使用起来也会有诸多限制,因此在不必要的情况下,不建议使用其事务机制。
接着再来说说MongoDB
的锁机制,由于其天生的分布式特性,所以内部的锁机制尤为复杂,MySQL
中有的锁概念,MongoDB
中几乎都有,为此,详细、深入的内容可参考:MongoDB锁机制,这里只简单列出手动操作锁的命令,如下:
# 获取锁
db.collection.fsyncLock();
# 释放锁
db.collection.fsyncUnlock();