83-MongoDB介绍

MongoDB介绍

行业应用场景:
游戏场景,使用 MongoDB 存储游戏用户信息,用户的装备、积分等直接以内嵌文档的形式存储,方便查询、更新
物流场景,使用 MongoDB 存储订单信息,订单状态在运送过程中会不断更新
以 MongoDB 内嵌数组的形式来存储,一次查询就能将订单所有的变更读取出来
社交场景,使用 MongoDB 存储存储用户信息,以及用户发表的朋友圈信息
通过地理位置索引实现附近的人、地点等功能
物联网场景,使用 MongoDB 存储所有接入的智能设备信息,以及设备汇报的日志信息,并对这些信息进行多维度的分析
视频直播,使用 MongoDB 存储用户信息、礼物信息等
性能优越:
在使用场合下,千万级别的文档对象,近10G的数据,对有索引的ID的查询不会比mysql慢
而对非索引字段的查询,则是全面胜出
mysql实际无法胜任大数据量下任意字段的查询(因为在没有索引的情况下,基本需要排除,因为不可能所有字段都设置索引)
而mongodb的查询性能实在牛x
MySQL、MongoDB简单的性能测试,网上看到一组测试数据,分享给大家
测试环境:Windows 10、内存8G、CPU i5 3.30GHZ,均无索引
测试语言:Python
链接工具:pymysql、pymongo
MySQL && Mongo 测试数据统计

83-MongoDB介绍_第1张图片

虽然MongoDB基本上比Mysql好,但有些领域还是Mysql好的,并不能完全替换
实际上无论是Redis还是Mysql还是MongoDB都有各自擅长的领域,因为各有优点,也有缺点
MongoDB的优点:
弱一致性(最终一致,或者说是最终一致性),更能保证用户的访问速度:
当我们说外部一致性时,是针对分布式系统所说的CAP理论中的一致性
简单来说就是如何使得多台机器的副本保持一致
实际上Mongodb只能做到最终一致性,总会有"不一致时间窗口"
这是由于Mongodb在更新操作的时候,需要将同样的更新复制到副本节点当中,在将更新后的数据同步到其他副本时(由于单个副本更新是原子的,就如mysql的写锁一样,所以不会出现问题,但是其他副本会)
由于这段同步的时间无法保证reader读到的一定是最新数据(正是因为这样,使得访问速度快,因为不用等待同步)
即只能保证返回目前大多数节点的所持有的数据
而不一定是最新的数据(比如,只有primary节点更新完成,其它所有secondary节点都还没有更新完成就获取了)
文档结构的存储方式,能够更便捷的获取数据:
对于一个层级式的数据结构来说,如果要将这样的数据使用扁平式的,表状的结构来保存数据
这无论是在查询还是获取数据时都十分困难
举例1:就拿一个"字典项"来说,虽然并不十分复杂,但还是会关系到"定义"、“词性”、"发音"或是"引用"等内容
大部分工程师会将这种模型使用关系型数据库 中的主键和外键表现出来
但把它看作一个"文档"而不是"一系列有关系的表"岂不更好
使用 "dictionary.definition.partOfSpeech=‘noun’"来查询也比表之间一系列复杂(虽然往往代价也很高)的连接查询方便 且快速
举例2:在一个关系型数据库中,一篇博客(包含文章内容、评论、评论的投票)会被打散在多张数据表中
在MongoDB中,能用一个文档来表示一篇博客, 评论与投票作为文档数组,放在正文主文档中
这样数据更易于管理,消除了传统关系型数据库中影响性能和水平扩展性的"JOIN(连接)"操作
以下是后面的例子(可以不必理会):
db.blogposts.save({ 
 title : "My First Post", 
    author: {name : "Jane", id :1},
 comments : [{ by: "Abe", text: "First" },{ by 
: "Ada", text : "Good post" }]
});
db.blogposts.find( { "author.name" : "Jane" } );
db.blogposts.findOne({ 
    title : "My First Post", 
    "author.name": "Jane",
    comments : [{ by: "Abe", text: "First" },{ by 
: "Ada", text : "Good post" } ]
});
db.blogposts.find( { "comments.by" : "Ada" } );
举例3:MongoDB是一个面向文档的数据库,目前由10gen(也可叫Mongo)开发并维护
它的功能丰富,齐全,基本完全可以替代MySQL
在使用MongoDB做产品原型的过程中,我们总结了MonogDB的一些亮点:
使用JSON风格语法,易于掌握理解:MongoDB使用JSON的变种BSON作为内部存储的格式和语法
针对MongoDB的操作都使用JSON风格语法,客户端提交或接收的数据都使用JSON形式来展现
相对于SQL来说,更加直观,容易理解和掌握
Schema-less,支持嵌入子文档:MongoDB是一个Schemafree的文档数据库
一个数据库可以有多个Collection,每个Collection是Documents的集合
Collection和Document和传统数据库的Table和Row并不对等,无需事先定Collection,随时可以创建
Collection中可以包含具有不同schema的文档记录,这意味着
你上一条记录中的文档有3个属性,而下一条记录的文档可以有10个属 性
属性的类型既可以是基本的数据类型(如数字、字符串、日期等)
也可以是数组或者散列,甚至还可以是一个子文档(embed document)
这样,可以实现逆规范化(denormalizing)的数据模型,提高查询的速度
内置GridFS,支持大容量的存储:
GridFS是一个出色的分布式文件系统,可以支持海量的数据存储
内置了GridFS的MongoDB,能够满足对大数据集的快速范围查询
内置Sharding :
提供基于Range的Auto Sharding机制:一个collection可按照记录的范围,分成若干个段,切分到不同的Shard上
Shards可以和复制结合,配合Replica sets能够实现Sharding+fail-over,不同的Shard之间可以负载均衡
查询是对 客户端是透明的,客户端执行查询,统计,MapReduce等操作,这些会被MongoDB自动路由到后端的数据节点
这让我们关注于自己的业务,适当的 时候可以无痛的升级
MongoDB的Sharding设计能力最大可支持约20 petabytes(PB,1PB=1024TB,1TB=1024GB),足以支撑一般应用
保证MongoDB运行在便宜的PC服务器集群上,PC集群扩充起来非常方便并且成本很低
避免了"sharding"操作的复杂性和成本
第三方支持丰富:
(这是与其他的NoSQL相比,MongoDB也具有的优势)
现在网络上的很多NoSQL开源数据库完全属于社区型的,没有官方支持,给使用者带来了很大的风险
而开源文档数据库MongoDB背后有商业公司10gen为其提供供商业培训和支持
而且MongoDB社区非常活跃,很多开发框架都迅速提供了对MongDB的支持
不少知名大公司和网站也在生产环境中使用MongoDB
越来越多的创新型企业转而使用MongoDB作为和Django,RoR来搭配的技术方案
MongoDB的缺点:
事务支持不友好:
所以事务要求严格的系统(如果银行系统)肯定不能用它
这点和优点中弱一致性(最终一致)是对应的,因为可能操作的数据还是同一个
占用空间过大 :
关于其原因,在官方的FAQ中,提到有如下几个方面:
1、空间的预分配:为避免形成过多的硬盘碎片,mongodb每次空间不足时都会申请生成一大块的硬盘空间
而且申请的量从64M、128M、256M那 样的指数递增,直到2G为单个文件的最大体积
随着数据量的增加,你可以在其数据目录里看到这些整块生成容量不断递增的文件
2、字段名所占用的空间:为了保持每个记录内的结构信息用于查询
mongodb需要把每个字段的key-value都以BSON的形式存储,如果 value域相对于key域并不大
比如存放数值型的数据,则数据的overhead是最大的,一种减少空间占用的方法是把字段名尽量取短一些
这样占用 空间就小了,但这就要求在易读性与空间占用上作为权衡了
我曾想过把字段名做个index,每个字段名用一个字节表示,这样就不用担心字段名取多长 了
但作者的担忧也不无道理,这种索引方式需要每次查询得到结果后把索引值跟原值作一个替换,再发送到客户端
这个替换也是挺耗费时间的,现在的实现算是拿空间来换取时间吧
3、删除记录不释放空间:这很容易理解,为避免记录删除后的数据的大规模挪动,原记录空间不删除
只标记"已删除"即可,以后还可以重复利用。
4、可以定期运行db.repairDatabase()来整理记录,但这个过程会比较缓慢
安装与配置:
安装:
官网: www.mongodb.org
下载社区版 MongoDB 4.1.3(但是我们可以直接使用Linux来进行下载)
[root@A opt]# wget https://fastdl.mongodb.org/linux/mongodb-linux-x86_64-4.1.3.tgz
#如果下载不了,那么去官网里自己下载一个这个Linux的版本,或者复制上面的网址看看可不可以下载来进行试验
#A是设置的编号,具体可以看81章博客
将压缩包解压即可:
[root@A opt]# tar -zxvf mongodb-linux-x86_64-4.1.3.tgz
启动:
[root@A mongodb-linux-x86_64-4.1.3]# ./bin/mongod #进入解压的目录里面
#此方式报错,因为mongo启动需要加载默认的数据目录和日志文件,当前并没有为此创建,一般默认是/data/db目录
#我们把各种参数都放在配置文件中,并在启动前创建好各种目录,后面会设置成下面这个目录,在这之前,还是启动不了的
[root@A opt]# mkdir -p /data/mongo/
配置文件样例:
在mongo的根目录下(好像随便一个目录都可以,因为只需要指定目录即可)创建配置文件mongo.conf:
[root@A mongodb-linux-x86_64-4.1.3]# vim mongo.conf
dbpath=/data/mongo/
port=27017
bind_ip=0.0.0.0
fork=true
logpath=/data/mongo/MongoDB.log
logappend=true
auth=false
/*
参数     说明
dbpath 数据库目录,默认/data/db
port 监听的端口,默认27017
bind_ip 监听IP地址,默认全部可以访问(可能默认的是本机,所以最好设置上)
fork 是否以后台启动的方式登陆
logpath 日志路径,一般会默认加上对应的文件
logappend 是否追加日志
auth 是否开启用户密码登陆(虽然能进入,但却不能访问)
config 指定配置文件,另外的一个参数说明
*/
指定配置文件方式的启动:
[root@A mongodb-linux-x86_64-4.1.3]# ./bin/mongod -f mongo.conf
使用mongo shell进入mongo(可以输入命令的客户端):
[root@A /]# ./bin/mongo #默认27017
指定主机和端口的方式进入mongo(跟上面一样的进入,只是命令不同):指定ip和端口
[root@A mongodb-linux-x86_64-4.1.3]# ./bin/mongo --host 192.168.164.128 --port=27017
#也可以./bin/mongo --host 192.168.164.128:27017,这样的省略写法
#也可以./bin/mongo --port=27017,不指定主机(因为默认本机),而指定的端口是启动时,配置的端口
#这样也可,./bin/mongo,虽然后面也可以随便加(因为不会被操作识别),即默认27017
Mongodb GUI(图形用户界面)工具:
MongoDB Compass Community :
MongoDB Compass Community由MongoDB开发人员开发,这意味着更高的可靠性和兼容性
它为MongoDB提供GUI mongodb工具,以探索数据库交互,具有完整的CRUD功能并提供可视方案
借助内置模式可视化,用户可以分析文档并显示丰富的结构,为了监控服务器的负载,它提供了数据库操作的实时统计信息
就像MongoDB一样,Compass也有两个版本,一个是Enterprise(付费),社区可以免费使用
适用于Linux,Mac或Windows
NoSQLBooster(mongobooster) :
NoSQLBooster是MongoDB CLI界面中非常流行的GUI工具,它正式名称为MongoBooster
NoSQLBooster是一个跨平台,它带有一堆mongodb工具来管理数据库和监控服务器
这个Mongodb工具包括服务器监控工具,Visual Explain Plan,查询构建器,SQL查询,ES2017语法支持等等…
它有免费,个人和商业版本,当然,免费版本有一些功能限制,NoSQLBooster也可用于Windows,MacOS和Linux
数据库基本操作:
下载图形化界面,下载地址:
官网:https://www.nosqlbooster.com/
由于前面的配置中,设置了不用账号密码登录,所有只需要连接即可

83-MongoDB介绍_第2张图片

点击左上角的三角形,然后出现From URL进入,将localhost修改成自己的对应启动MongoDB的主机即可连接(可以进行测试)
连接后出现:

83-MongoDB介绍_第3张图片

点击主机名称那里,右键,选择Run SQL Query…即可打开命令行的操作
注意:这个榆Mysql不一样,这是肯定的,基本没有什么结束符号
比如show dbs,可以执行,但是show dbs;不能执行,多一个分号,所有最好不要加分号,下面的对应命令中,也基本并不会加分号
若有分号,可以自行删除,当然有些是可以加的
因为有自带结束的操作,比如db.dept.find(),且有些自带开始,比如var,实际上基本都会自带开始
但最好是不加,且要换行的执行,这样基本不会出现问题
查看数据库:
show dbs
切换数据库 (对于sql界面来说的显示,可以将MongoDB的命令看成sql)
如果没有对应的数据库则创建(新建的库默认不显示,必须要插入一个文档也就是数据后才会显示,自连接的客户端也是一样的)
use 数据库名 #假设该数据库是laosun,当没有指定数据库时,默认指向test库,也就是说相当于默认执行use text
#下面这个laosun是集合,不是数据库
db.laosun.insert({"did":1,"dname":"开发部","loc":"A区1座"}) #必须在该数据库里插入数据,才会显示出来
#即虽然可以use laosun,但是show dbs或者直接的观看都不会显示
#实际上我们添加该数据时,若没有对应集合,会自动创建(这就是为什么集合和数据的创建不分先后的主要原因)
数据库里有集合(可以看成表,集合里面有数据,上面的命令中{}里面的数据)
创建集合:
db.createCollection("集合名")
查看集合(下面两条命令结果一样):
show tables
show collections
删除集合:
db.集合名.drop() #若数据库里没有集合,一般不会显示数据库了
删除数据库:
db.dropDatabase()
#虽然当前还是在原来数据库,实际上可以看成缓冲或缓存
#这时db命令的执行,也是当前的库,即正是由于还在,若没有切换数据库的话
#那么接下来的操作,比如添加集合,基本都会先使得创建该数据库,并添加集合,有数据再添加数据
#除非已经切换了库
添加:
插入一条数据,自动创建dept集合(集合中的数据格式,就是json格式)
db.dept.insert(
   {
       "did":1,
       "dname":"开发部",
       "loc":"A区1座"
   })
查看集合中的所有数据:
db.dept.find()
插入数据:
var dataVar = {
 "did":2,
 "dname":"市场部",
 "loc":"深圳",
 "count":20
}
db.dept.insert(dataVar)
#注意:上面的命令需要一起执行,这样dataVar才会被获得,否则分开执行的话
#db.dept.insert(dataVar)中的dataVar会显示没有,因为分开的执行结果不共享
数据随便定义,所以基本没法(有)查看集合结构的方法,没有固定结构:
var dataVar = {
 "did":2,
 "dname":"市场部"
}
db.dept.insert(dataVar)
MongoDB每一行记录都有 _ id,这个_ id是由 时间戳 + 机器码 + 进程的pid + 计数器
这个_id值是绝对不会重复(基本上)的,这个ID信息是MongoDB为用户服务的,不能被改变
插入数组数据:
db.dept.insert([
 {"did":3,"dname":"人事部"},
 {"did":4,"dname":"保洁部"}
])
#相当于db.dept.insert({"did":3,"dname":"人事部"})和db.dept.insert({"did":4,"dname":"保洁部"})
#分开执行两次,若删除[],那么只有第一个的{}会执行,其他不会,自带结束
    #所以db.dept.insert({"did":3,"dname":"人事部"}也可

#插入100条数据
for(var i = 1; i<100;i++){
 db.dept.insert({"did":i,"dname":"测试"+i+"部"});
}
#这里可以使用一些逻辑语句进行执行,比mysql的逻辑执行简单很多
#实际上最好不要再数据中操作逻辑,这些操作一般在后台执行对应的操作,更加简便,且清晰,也好维护
#虽然会慢一点,特别是距离过大时,会出现明显的速度慢
MongoDB是部分显示数据,一般默认每页显示20条数据
其中使用查询语句一般可以全部查询,但直接的点击进去,一般只会显示100条数据(应该可以改变)
假设忘记了命令,那么可以点击如下:

83-MongoDB介绍_第4张图片

可以看到命令的模板,一般是根据当前谁打开的命令界面进行模板的默认,可以自己测试
最后注意: " 和 ’ 都可以当成字符串的符号,但不能交替使用,且key-value中,key不使用的话
一般会默认变成字符串(可以相当于默认加上""),但value却要防止变量的识别,即一般不能写字母
删除 :
remove( ) 有两个可选项:
满足条件的删除
是否只删除一条,true或1,就是删除一条
删除指定的某个数据:
db.dept.remove({
    "_id" : ObjectId("6114d0d2430f6b4dcc876c41") #记得写上自己的_id
})
#如果是如下:
 db.dept.remove(
 {id:3}, 
 {justOne: 1}
 )
 #或者db.dept.remove(
 {id:3}, 
 1
 )
 #那么就只会删除1条(最前面的),当然其中的1可以是true,也是删除1条
 #实际上对应的值好像可以随便写,只要符合即可,就只会删除一条了,只是1和true好观察
 #若不是后面的那个格式,根据前面的说法,那么也就只会识别第一个{},即会删除全部id为3的数据
删除名称中含有"测试"的数据,默认会将匹配的数据全部删除:
db.dept.remove({
    "dname" : /测试/
})
#删除包含dname的值是测试的数据
删除名称中含有"测试"的数据,要求只删除一条:
db.dept.remove({
    "dname" : /测试/
},true)
清空集合中的全部数据:
db.dept.remove({}) #也就是符合全部的
删除集合:
db.dept.drop()
查看当前所在的数据库:
db
删除当前所在的数据库:
db.dropDatabase()
修改:
关系型数据库的数据更新,对于Mongo而言,最好的办法不是修改,而是删掉,重新添加
若要进行修改,那么可以输入如下命令:
db.laosun.update(
{"id":55}, #要修改的id值
{"id":88} #要修改成的值,即55变成88,实际上也是可以修改id的,即第二个参数的key是uid的话,那么就修改成uid了
)
#但是上面只会修改一个,默认一个,从头开始,从上到下(对于这样的,一般都是对于find查询的先后加入顺序来说)
#因为可能直接的查看是根据后入在前的方式(可能可以改变)
#若要修改多个,可以输入如下:
db.laosun.update(
{"id":55},
{$set:{"id":88}}, 
#要操作多个,需要是$set开始才可会选择全部的设置,且由于他是设置
#那么他并不会替换原来的key,即若有相同的key,则设置,不同则创建(添加)
#这就是使用$set的与不使用的区别所在,可以自己测试
false, 
true
)
实际上上面的更新操作用的就是函数,接下来说明一下该函数
函数:
update语法:
db.集合.update(更新条件 , 新对象数据 , upsert , multi)
upsert:函数的动作,修改还是添加,有如下的两个参数:
false:如果没有匹配的数据,啥也不干
true:如果没有匹配的数据,执行添加操作
multi:如果条件匹配多个结果,有如下的两个参数:
false:匹配项只修改一行,默认的
true:匹配项全部修改
实际上大多数的参数都是{}的,而不是字符串和数组
修改数据根据_Id:
#插入一条
var dataVar = {
 "id":1,
 "name":"吕布",
 "age":29,
    "address":"灵界",
}
db.student.insert(dataVar)

#修改
var dataNew = {
 "id":1,
 "name":"吕小布",
 "age":29,
    "address":"灵幻界",
}
db.student.update({
    "_id": ObjectId("6114df1d430f6b4dcc876c42"), 
},dataNew)
修改年龄27岁的人的地址为"大东北":
db.student.update(
   {"age":27},
   {"$set":{"address":"大东北"}},
    false,
    false #这个true,需要$set的格式,来使得全部选择替换
)
修改99岁的名字为"上仙",如果没有99岁的人,则添加本条数据
db.student.update(
   {"age":99} , 
   {"$set":{"name":"上仙"}},
    true, 
    #若没有$set,那么第一个参数不会添加,只会添加第二个参数,加上后,就会添加,且对应的{}不能是[]包括的
    #否则执行不了,因为有些地方不识别[],只识别{},比如insert识别[],而update不识别[]
    false
)
只修改 27岁的第一条 地址为"东北三省"
db.student.update(
   {"age":27},
   {"$set":{"address":"东北三省"}},
    true,
    false
)
修改全部 27岁的地址为"东北三省":
db.student.update(
   {"age":27},
   {"$set":{"address":"东北三省"}},
    true,
    true
)
修改器:
初始化一条测试数据:
db.student.insert({
    "name":"地三鲜",
    "age":88,
    "score":99
})
$inc主要针对数字字段,增加某个数字字段的值,如果不是数字字段,那么会报错
当然需要首先匹配成功再说(第一个参数,一般指方法里的第一个{},不包括里面的),因为先后顺序
语法:{“$inc”:{“成员”:增减量}
修改年龄88岁的人,年龄+2岁,评分-10分
db.student.update(
   {"age":88},
   {
        "$inc":{
            "age":2,
            "score":-10
       }
   }
)
$set对内容重新设定(相同的设置,不同的创建):
语法:{“$set”:{“成员”:新的内容}
将90岁的人,评分设置为90分
db.student.update(
   {"age":90},
   {
        "$set":{
            "score":100
       }
   }
)
#其实也相当于进行更新,只是他可以支持后面的upsert参数和multi参数
$unset删除某个成员:
语法:{“$unset”:{“成员”:1}
删除”地三鲜”的年龄和分数信息
db.student.update(
   {"name":"地三鲜"},
   {
        "$unset":{
            "age":1, 
            "score":1
            #实际上对对应的value值基本不做要求,比如12,"a",都可,只看key
       }
   }
)
$push将指定内容追加到指定成员(基本上是数组,追加的基本都是数组):
语法:{“$push”:{“成员”:value}
给"地三鲜"添加一门"毛概"(此时地三鲜没有课程信息)
db.student.update(
   {"name":"地三鲜"},
 {
        "$push":{
          "subject":"毛概" #给subject追加或者添加(没有的话相当于添加,后面的追加也是如此)
          #是数组(基本上),,即使用数组包括subject的value值,无论该值是否是数组
          #如果该值就是普通value,那么取出直接显示,从0开始,如果是{},那么取出,再取出直接显示,都从0开始
          #若是[],那么取出,再取出,最后再取出直接显示,都从0开始,从0开始是相对的
       }
   }
)
给"地三鲜"追加一门"邓论"
db.student.update(
   {"name":"地三鲜"},
   {
        "$push":{
            "subject":"邓论"
       }
   }
)
$pushAll追加数组的方式已经在高版本中淘汰了(好像将pushAll来替换each后,执行会报错)
可以使用$each添加一个数组到属性值中
添加一道菜"毛血旺",在多追加两种配菜"[“豆腐”,“油菜”]"
db.student.insert({"name":"毛血旺"})
  db.student.update(
     {"name":"毛血旺"},
     {
          "$push":{
              "cai":{
                  "$each":["豆腐","鸭血","豆芽"] #没有的话相当于添加
                  #使用这个,那么默认使用["豆腐","鸭血","豆芽"]里面的值,放入数组里面,相当于遍历
                  #但是要注意:必须加上[],因为基本必须要是数组开始,否则报错,这是$each的作用
                  #虽然值可以是数组,实际上key也可以是数组(里面只能是一个字符串)
                  #只是任然是值,但不能是{},或者是多个[]里面的值
                  #否则报错,而单独的字符串,实际上也是可以添加的,每个字符当成一个值,从0开始
                  #实际上他们只是满足了json的格式
             }
         }
     }
 )
 #上面注释之所以说那样的解释,是因为如下:
  db.student.update(
     {"name":"毛血旺"},
     {
         "$push":{
             "caiii9":["豆腐","鸭血","豆芽"]
            
         }
     }
 )
 #这个的结果是[["豆腐","鸭血","豆芽"]],而使用$each,结果就是他的value值,即["豆腐","鸭血","豆芽"]
 #[]是追加时加的,而不是$each的,只是取出$each的[]里面的值
$addToSet追加到数据内容,先判断是否存在,如果存在,不做任何修改,如果不存在,则会追到到数组内容中
语法:{“$addToSet”:{“成员”:内容}
向"毛血旺"多加一道配菜"豆腐"
db.student.update(
   {"name":"毛血旺"},
   {
      "$addToSet":{
            "cai":"豆腐" #没有的话相当于添加
       }
   }
)
#因为上一次操作,毛血旺配菜的数组中已经存在豆腐了,所以没变化
#再追加油菜,而油菜是不存在的,所以油菜被追加进去了
db.student.update(
   {"name":"毛血旺"},
   {
        "$addToSet":{
            "cai":"油菜"
       }
   }
)
$pop删除数组内的数据:
语法:{“$pop”:{“成员”:数组}
删除地三鲜的第一门课程
db.student.update(
   {"name":"地三鲜"},
 {
        "$pop":{
          "subject":-1
       }
   }
)
删除地三鲜的最后一门课程:
db.student.update(
   {"name":"地三鲜"},
   {
        "$pop":{
            "subject":1
       }
   }
)
1就是最后,-1就是第一
$pull从数组中删除指定数据,存在,删除,不存在,没变化
语法:{“$pull”:{“成员”:数组}
删除毛血旺配菜中的鲍鱼,不存在鲍鱼,所有没变化
db.student.update(
   {"name":"毛血旺"},
 {
        "$pull":{
          "cai":"鲍鱼" #若匹配所有,所有鲍鱼都会删除,与push类似,将值看成一体
       }
   }
)
删除毛血旺配菜中的油菜,油菜存在,所有删除
db.student.update(
   {"name":"毛血旺"},
   {
        "$pull":{
            "cai":"油菜" #若匹配所有,所有油菜都会删除
       }
   }
)
$pullAll一次性删除多个内容
语法:{“$pullAll”:{“成员”:[“值1”,“值2”,…]}
删除孙大仙的毛概和计算机两门课
db.student.insert({
    "name":"孙大仙",
    "subject":["毛概","邓论","计算机"]
})
db.student.update(
   {"name":"孙大仙"},
   {
        "$pullAll":{
            "subject":["毛概","计算机"] #也要上数组的形式
       }
   }
)
$rename为成员名称重命名
语法:{“$rename”:{“旧的成员名称”:“新的成员名称”}
将毛血旺的 “cai"改成"配菜”
db.student.update(
   {"name":"毛血旺"},
{
        "$rename":{
            "cai":"配菜"
       }
   }
)
实际上:key可以[]单个字符串,其他基本不可以,且对应的key-value好像自带结束
也就是说执行到上面的配菜后面的 " 就可以了,可以自己尝试一下(可能是语法不严谨,或者自带补全等等)
查询:
语法:db.dept.find({查询条件})
查看一条数据:
db.dept.insert({
    "did":1,
    "dname":"开发部",
    "address":"北京海淀"
})
db.dept.insert({
    "did":2,
    "dname":"市场部",
    "address":"上海虹桥"
})
db.dept.findOne() #查询第一个加入的,也就是第一个,自带类似于pretty() 的效果,也就是会格式化bson
查询did=1的数据:
db.dept.find({
    "did":1 #所有属性都默认显示,也就是默认都不为0的显示,下面的投影会说明
})
投影查询(查询部分属性,控制属性的显示)did=1的数据,其中控制属性的显示的对于key的值可以是,0不显示,1显示
实际上除了0,其他的只要可以符合写入,那么都算作显示,比如2,-1,"a"等,只是1更加直观
投射里,要么都不是0(一般都设置为1),要么全是0
否则就报错:“Projection cannot have a mix of inclusion and exclusion.”(投影不能同时包含和排除)
可能_id不受限制(但通常都会受限制)
db.dept.find(
   {"did":1},
   {"dname":1} #指定了显示的属性,即这里只显示dname属性的结果
)
db.dept.find(
   {"did":1},
   {"dname":1,"address":1} #这里都设置为1,来显示
)
#实际上第二个参数也就是可以是多个的显示属性的,用{}包括
漂亮的显示,列太少看不出来效果,不妨自己动手,多创建些列,使得列多一些
在命令行模式下,会格式化bson
db.dept.find(
   {"did":1},
   {"dname":1,"address":1}
).pretty() 
#记得在命令行模式下测试(不是图形界面),因为命令行模式下,一般都会是一行一行的
#而使用了pretty()就会使得变成对应的json的显示,最好自己测试
findOne()自带格式化功能,也是列多才有效果
db.dept.findOne()
关系运算:

83-MongoDB介绍_第5张图片

初始化数据:
db.student.drop();
db.student.insert({"name":"张三","sex":"男","age":19,"address":"高碑店"})
db.student.insert({"name":"李四","sex":"女","age":18,"address":"海淀区"})
db.student.insert({"name":"王五","sex":"女","age":21,"address":"东城区"})
db.student.insert({"name":"赵六","sex":"女","age":21,"address":"高碑店"})
db.student.insert({"name":"孙七","sex":"男","age":22,"address":"东城区"})
db.student.insert({"name":"田八","sex":"女","age":22,"address":"王府井"})
db.student.insert({"name":"钱九","sex":"男","age":23,"address":"东城区"})
db.student.insert({"name":"周十","sex":"男","age":17,"address":"高碑店"})
查询女同学:
db.student.find({
    "sex":"女"
})
查询年龄大于18岁的学生(json格式中套用json格式),实际上大多数的参数都是{}的:
db.student.find({
    "age": {
        "$gt": 18
   }
})
#18岁以上的女同学
db.student.find({
    "age": {
        "$gt": 18
   },
    "sex":"女"
})
#条件是可以多的,也就是参数是可以多的
逻辑运算:
/*
与$and,或$or,非$not/$nor
*/
//查询年龄在18到20之间的同学
db.student.find({
    "age": {
        "$gte": 18,
        "$lte": 20 
         //参数可以多加,一起操作,只要有一个没有满足,那么就不算,比如18变成21,那么就基本不可能出现对应结果
         //因为基本没有大于等于21,且小于等于20的对应结果
   },
})
18岁以上,或者性别是女的同学
db.student.find({
  "$or":[
     {"age":{"$gt":18}}, 
     {"sex":{"$eq":"女"}}
     #虽然{}里面不能直接包含{},但value可以,只是这里并不可以这样操作,一般我们通常使用[]来表示多个{}
   ]  
})
#相当于数组里面的结果用或包括,其中可以不使用$eq的(相当于=),直接等于(key:value)也可

#实际上find可以有多个参数,即每个参数之间相当于$or
#22岁以下,性别是男的学生($nor指的是对$or操作取反)
db.student.find({
  "$nor":[
     {"age":{"$gt":22}},
     {"sex":{"$eq":"女"}}
   ]  
})
#查询不是20岁以下的学生
db.student.find( {
    "age":{
     "$not":  { #用在里面,且value必须是{},否则一般会报错
         "$lt":21
     }
    }
})
#实际上$nor可以看成全局的取反,而$not则是局部的取反
求模:
求模(求余数)
年龄正好是2的倍数,也就是age%2 == 0
db.student.find({
    "age":{
        "$mod":[2,0] 
        #其中age%2==0中的2和0
        #2代表[2,0]中的2
        #0代表[2,0]中的0
   }
})
#age%20==1
db.student.find({
    "age":{
        "$mod":[20,1]#虽然[2,0]结果也是一样,但大基数的数据下,实际上会有很大差别
        #因为20的除下去,始终不完全是2的除下去,即20并不是2的倍数
   }
});
#数组不只是可以操作{}对象,也是可以操作字符串的,一般多个的字符串匹配都会使用数组
#他们都是一种数据集合,数组基本全包括,{}的value全包括
范围:
只要是数据库,必然会存在这些操作(下面两个):
在范围内 $in
不在范围内 $nin
姓名是张三,李四,王五的范围查询
db.student.find({
    "name":{
        "$in":["张三","李四","王五"]
   }
})
范围取反:
db.student.find({
    "name":{
        "$nin":["张三","李四","王五"]
   }
})
数组查询 :
初始化数据:
db.student.insert({"name":"老孙 - A","sex":"男","age":17,"address":"中关村1",
"subject":["语文","数学","英语"]})
db.student.insert({"name":"老孙 - B","sex":"女","age":18,"address":"中关村2",
"subject":["语文","数学","英语","物理"]})
db.student.insert({"name":"老孙 - C","sex":"男","age":19,"address":"中关村3",
"subject":["语文","数学","英语","物理","化学"]})
db.student.insert({"name":"老孙 - D","sex":"女","age":20,"address":"中关村4",
"subject":["物理","化学"]})
db.student.insert({"name":"老孙 - E","sex":"男","age":21,"address":"中关村5",
"subject":["语文","数学","化学"]})
查询同时参加 语文 和 数学 课程的学生:
db.student.find({
    "subject" : {
        "$all" : ["语文","数学"] 
        #$all的值好像只能是数组,实际上像范围的一般也只能操作数组,当然,若只用一个,可以看下面的写法
        #包含对应值的都查询,实际上单个字符串也可以
        #但由于subject是数组,那么都会匹配到,除非value也是对应数组
   }
})
查询地址是"中关村3"的学生的另一种写法:
db.student.find({
    "address" : {
        "$all" : ["中关村3"]
   }
})
现在,集合中保存的信息是数组信息,那么我们就可以使用索引查询了key.index
查询第二门(index=1)课程是数学的学生
db.student.find({
    "subject.1" : "数学" 
    #使用了索引,那么就必须加上分号了,因为不加会影响语法
    #这里代表数组下标1的值是数学的查询,下标自然也是从0开始
})
查询只参加两门课程的学生:
db.student.find({
    "subject" : {
        "$size": 2 #对应的数组只有两个值的查询,好像只作用于数组
   }
})
有些的$操作,一般格式最好不要修改,数组就是数组,单个值就是单个值
作用对象也不要进行修改,不同的对象,一般不会操作,甚至可能报错
比如$size一般只会操作数组,若操作字符串,若是负数一般会报错
因为他们本来就是这样的,一般修改后,可能不会操作(比如单个值,看到数组或者{},一般会当成整体),甚至会报错
所有最好不要修改他们规定的格式
查询年龄19岁,但是课程只返回前两门课程,使用$slice关键字:
db.student.find(
   {"age":19},
   {
    "subject" : {
        "$slice":2 
        #返回数组的前两个
        #下标0和下标1,超出的自然全显示
        #若是0,则就是[]
        #若是负数,则从后面开始数,超出的自然也是全部显示,具体自己进行测试
   }
})
返回后两门课程:
db.student.find(
   {"age":19},
   {
    "subject" : {
        "$slice":-2 
        #从后面开始,比如[1,2,3]
        #3的下标是-1,2的下标是-2
        #所以返回[2,3]
        #若是-1,则返回[3]
   }
})
返回中间的课程 索引1开始(包含索引),返回2门课:
db.student.find(
   {"age":19},
   {
    "subject" : {
        "$slice": [1,2] #这个2虽然是返回索引开始的数量,但基本始终从左到右,无论索引是否是负数
   }
})
嵌套集合 :
初始化数据,养宠物
db.student.insert({
    "name":"孙大仙",
    "sex":"男",
    "age":31,
    "address":"天宫院",
    "subject":["java","mongo","html"],
    "pets":[
       {
            "name":"大G",
            "age":3,
            "brand":"狗"
       },
       {
            "name":"大咪",
            "age":4,
            "brand":"猫"
       }
   ]
})

db.student.insert({"name":"乔丹","sex":"男","age":47,"address":"芝加哥","subject":["数学","英语"],
"pets":[
 {"name":"杰瑞","age":2,"brand":"鼠"},
 {"name":"山姆","age":3,"brand":"猫"}
]})

db.student.insert({"name":"kobe","sex":"男","age":40,"address":"洛杉矶","subject":["英语","篮球"],
"pets":[
 {"name":"克里斯提娜","age":2,"brand":"金丝雀"},
 {"name":"雪瑞","age":12,"brand":"马"}
]})
查询20岁以上并且养猫的人:
db.student.find(
   {
        "age": {
            "$gte":20
       },
        "pets" : {
            "$elemMatch" : { #可以操作当前数组里的{}里面的匹配值
                "brand":"猫"
           }
       }
   }
)
字段是否存在判断 :
查询养宠物的学生(不养的就是false)
db.student.find({
    "pets":{
        "$exists" : true #当前字段是否存在
   }
})
条件过滤where:
年龄40岁以上的学生
db.student.find({
    "$where":"this.age > 40" #必须加上this,这好像是规定
})

db.student.find({
    $where: "this.age > 40"    #$where 没加引号也可以,这是自然的,默认加上引号(一般对符合格式的key来说)
})

db.student.find("this.age>=40");   #虽然报红色浪线,但并不影响执行,好像也当作条件(默认加上)
#因为他并不会像java一样操作编译,而是直接的执行,即他也只是检测而已
查询30到40岁之间的同学:
db.student.find({
    $and:[
       {$where:"this.age > 30"},
       {$where:"this.age < 40"}
       #实际上只要是可以当成条件的,一般都可以操作
   ]
})
这样的写法,可以实现数据的查询
但是最大的缺点是将MongoDB中的BSON数据转成了Javascript语法结构循环验证(比如this.age > 30)
这样的话,不方便使用数据库的索引机制,并不推荐使用
因为MongoDB中索引的效果,对查询的提升是相当明显的
正则运算:
这里想模糊查询,基本必须要使用正则
姓名包含孙字的学生(不要加双引号):
db.student.find({
    "name" : {
        "$regex" : /孙/ 
        #包含的意思,相当于mysql里的 %孙%
        #好像MongoDB只有包含的模糊查询,若有其他的可以自行百度进行查看,这里就不写上了
        #不要加双引号,看成完全的整体的(完全的),//是一体的,不能是单独,所以特殊
   }
})
以下用简略的写法:
db.student.find({
    "name" : /孙/ #正是因为这样的特殊,所以可以省略
})
姓名中包含字母a,忽略大小写:
db.student.find({
    "name" : /a/i 
    #i使得忽略大小写,这里可以将name修改成subject,测试java或者jaVa来进行验证
    #name不好验证(基本验证不了,因为是汉字)
    #后面的i不能随便写,基本只能识别对应有限的符号,比如1就会报错
})
数组查询也同理,查询学习课程有化字的
db.student.find({
    "subject" : /化/ 
    #若是数组,那么会依次取出对比,当然由于{}是对应的对象数据,那么不会进行操作,或者说直接跳过
})
排序:
年龄升序排列:
db.student.find().sort({
    "age" : 1 
    #只能是1或者-1,其他基本都会报错
    #使用1,那么查询的结果就是从小到大(对查询结果来说,从上到下的看)
    #使用-1,那么查询的结果就是从大到小(对查询结果来说,从上到下的看)
})
年龄降序排列:
db.student.find().sort({
    "age" : -1
})
自然排序:数据保存的先后顺序
最新添加的靠前(从上到下的看,靠前):
db.student.find().sort({
    "$natural" : -1
    #只能是1或者-1,其他基本都会报错
    #设置1,那么就相当于db.student.find()(默认新的在后面),也就是新的在后面
    #设置-1,那么就是新的在前面,这就是直接表的显示
})
分页:
skip(n):跨过多少行 (相对于下标从0开始来说,正好就是,比如说跨0行,那么下标也就是0,跨5行,那么下标也就是5了)
limit(n):每页数量
年龄排序:第1页
db.student.find().skip(0).limit(5).sort({
    "age":1
})
#先进行排序,然后根据对应位置进行显示
#skip(0)中0就代表着,从上到下的第一条数据(下标为0)
#limit(5)就代表着,开始往下显示几条(包括0的那个数据)
年龄排序:第2页
db.student.find().skip(5).limit(5).sort({ 
#之所以是skip(5),是因为0,1,2,3,4代表第一个页,也就是5条数据,对于这里来说的
    "age":1
})
#所以实际上skip(n)的n若需要确定是那页的第一个,可以是如下公式(假设每页是5):
#(页码-1) * 每页数量,当页码为2时,那么就是5,正好是第二页的第一条
#当页码为3时,那么就是10,正好是第三页的第一条(5,6,7,8,9是上一页的)
#后面可以自己进行推断,一般用在程序里面进行分页
游标 :
所谓的游标,就是让数据一行行的操作,类似与resultSet数据结构
使用find()函数返回游标 var you = db.student.find()
要想操作返回的游标,我们有两个函数可以使用
hasNext() 判断是否有下一行数据(在前面开始,类似于java的迭代器)
next() 取出当前数据
循环取值:
var you = db.student.find()
while(you.hasNext()){
    print(you.next().name)
}
按照json格式打印:
var you = db.student.find()
while(you.hasNext()){
 printjson(you.next())
}
索引:
与以往的数据库一样,加快检索性能
索引分为两种,一种自动创建的,一种手动创建的
一般索引可以先行创建,而不用先创建数据,因为他只是用来进行操作的(对比操作)
且索引创建时,若没有对应的集合,那么也会顺便创建出来对应集合
准备一个简单的集合:
db.person.insert({"name":"吕布","sex":"男","age":19,"address":"高碑店"})
db.person.insert({"name":"赵云","sex":"女","age":18,"address":"海淀区"})
db.person.insert({"name":"典韦","sex":"女","age":21,"address":"东城区"})
db.person.insert({"name":"关羽","sex":"女","age":21,"address":"高碑店"})
db.person.insert({"name":"马超","sex":"男","age":22,"address":"东城区"})
db.person.insert({"name":"张飞","sex":"女","age":22,"address":"王府井"})
db.person.insert({"name":"黄忠","sex":"男","age":23,"address":"东城区"})
db.person.insert({"name":"夏侯惇","sex":"男","age":17,"address":"高碑店"})
查看当前集合下的索引:
db.person.getIndexes()
结果:

83-MongoDB介绍_第6张图片

v:版本,1代表是升序,name:索引名,ns:所属数据库的集合
我们发现,有默认的索引,即对应的_id是升序的
这大概是作用与没有设置排序的查询吧,因为直接的查看,是降序,而find()就是升序
创建一个索引:
语法:db.person.ensureIndex({列:1})
为age列创建一个降序索引
db.person.ensureIndex({
    "age" : -1 #这个值代表索引的区别,这个值不同,那么虽然key相同,但索引还是不同的
    #实际上也可以设置多个键
})
#创建一样的,那么覆盖,_id是则是跳过,否则添加(默认的索引_id除外)
#当然,不能是0,负数应该是降序,正数应该是升序,好像小数也可
#若是字符串text,那么相当于全局的参数,其他字符串基本报错(包括数字字符串,也是字符串的一种)
#除非特殊的字符串,后面会有讲到的,如2d索引等等
#text是后面的全局索引的作用(全局索引里也会说明)
#实际上一般小数都只会取整数,很少情况下会报错,但对于值来说,一般都可以取小数
#比如这里或者添加数据,而分页则是取整数
再次查看:索引的名字是自动命名的
删除一个索引:
db.person.dropIndex({
    "age" : -1 #需要有对应索引,否则报错
})
#自动的命名一般是age_-1,即将key和value中间使用_连接(大多数是这样)
#而默认的则是固定的,即特殊的key_,没有value,所以是_id_
删除全部索引,默认的索引不能删除
db.person.dropIndexes()
唯一索引 :
用在某一个字段,让该字段的内容不能重复
让name不能重复,添加重复的name数据,报错
db.user1.insert({
    "name":"a1"
})
db.user1.find()
db.user1.ensureIndex(
   {
        "name":1
   },
   {
        "unique":true #若有相同的数据,那么不会添加成功,即报错
        #代表唯一,不能操作设置了不同unique的索引,否则报错,相同的会覆盖
        #无论是否相同,都对_id始终报错
        #unique默认是false不唯一
        #当有多个索引时,都进行操作,所以只要有一个所以是true,那么对应的参数就不能唯一
   }
   #这里的参数是分开的,不能放在一个{}里面,第二个参数里面可以放多个索引的操作
   #比如:{"unique":true,"expireAfterSeconds":5},实现不重复,且过期的索引
   #若多个索引都是一样的,那么在后面的覆盖前面的,比如{"unique":true,"unique":false},那么就是false
)
过期索引:
程序会出现若干秒之后,信息删除
这个间隔时间大多数情况并不是很准确
设置10秒后,索引过期(如果觉得时间太长,可以自己调低一点)
db.phones.ensureIndex(
   {"time":1}, #对应的值只能是除了0以外的数字,字母不可,只要成功,那么就可以设置过期时间
   {"expireAfterSeconds":10} 
   #虽然说明是10秒,但是却未必是成效的
   #这个只是针对于time是时间的情况,所以没有对应的数据,也行
)
添加数据:
db.phones.insert({"num":110,"time": ISODate("2020-01-01T22:55:13.369Z")})
db.phones.insert({"num":119,"time": ISODate("2020-01-02T22:55:13.369Z")})
db.phones.insert({"num":120,"time": ISODate("2020-01-03T22:55:13.369Z")})
db.phones.insert({"num":114,"time": new Date()})
10秒(时间不一定准确,应该说一段时间后)后,再次查询,数据全都消失了
对于过期索引,有如下要注意:
/*
1:存储在过期索引字段的值必须是指定的时间类型
说明:比如是ISODate或者ISODate数组,不能使用时间戳,否则不能被自动删除
比如说:db.phones.insert({time:1}),这种是不能被删除的
过期索引中最好的时间类型是ISODate(0时区的时间),0时区到Date会有一定的换算程序
所以比Date删除的时间偏差更大,即更久 (但会被删除)
2:如果指定了ISODate()数组,则按照最小的时间进行删除
3:过期索引基本不能是复合索引,因为我们不能指定两个过期时间索引,实际上好像可以,根据时间小的操作
4:删除时间不是精确的
说明:就算10秒到了,但删除过程是由后台程序每60s跑一次,而且删除也需要一些时间,索引存在误差
*/
MongoDB的注释就是 //
全文索引:
在以往的应用中,经常会用到模糊查询,而模糊查询往往并不是很准确,因为他只能查询A列或者B列
简单来说,就是只能操作一个字段,而不能操作多个字段,为了可以操作多个字段,则出现了全文索引
全文索引就来解决这个问题
创建一个新的集合:
db.news.insert({"title":"NoSQL","nei":"MongoDB"})
db.news.insert({"title":"js java","nei":"前端技术"})
db.news.insert({"title":"编程语言","nei":"java"})
db.news.insert({"title":"java","nei":"好语言"})
db.news.insert({"title":"java","nei":"java"})
设置全文索引:
db.news.ensureIndex({
   "title":"text", 
   "nei":"text"
   #其中对应的字段需要存在,否则报错,因为是全局的了,防止特殊情况
   #设置后,我们可以通过$text来表示这两个字段,即设置了全局索引
})
进行模糊查询
/*
下面的解释,根据后面的代码可以更好的理解
全文检索$text判断符,数据查询$search运算符
查询指定关键字: {"$search" : "关键字"}
查询多个关键字(或关系): {"$search" : "关键字1 关键字2 ..."} 空格
查询多个关键字(与关系): {"$search" : "\"关键字\""} 双引转义,使用了\"转义成"

注意:关键字好像只能是文本(字符串或者数字),{}和[]好像不可以操作
*/
查询单个内容:
db.news.find({
    "$text":{ #$text代表前面两个字段,或者说键
        "$search":"java" 
        #注意他基本只会查询对应的整个单词,部分不会匹配,比如若有java,而你写上av,那么查询不到
        #在使用全文索引中,要进行模糊查询,需要$search
        #这个代表着,只要$text对应的字段(也可以叫做键)中
        #有一个符合$search的值,这里是java,那么就显示,所以会显示4个数据
        #这是$text和$search的作用,并不是$search的value的作用,之所以这样说,看后面的代码操作
   }
})
查询java或js的新闻:
db.news.find({
    "$text":{
        "$search":"java js" #这里写上两个数据,根据前面的解释,那么就算或的作用
        #这个或的意思是对于java和js来说的
        #即只要$text对应的两个键值只要有一个键值是符合java或者js都算是查询结果
   }
})
查询同时包含java和js的新闻:
db.news.find({
    "$text":{
        "$search":"\"java\" \"js\""    #两个并且的关键字之间的空格有没有都可以,因为""自带分开
        #简单来说,实际上也可以是'"js" "java"',只是转义使得"共存而已
        #也就是说,只要他们是""包括的,那么就是与,当然,若是''包括的不算
        #即"'js' 'java'",相当于"js java",即或的操作   
        #只要不是""包括的,那么都直接是对应的值,即或的操作,这是有无空格就有必要了,因为没有空格会看成一体
        
        #这样只要$text的其中一个键值符合就算是查询结果
   }
})
查询包含java,但是不包含js的内容:
db.news.find({
    "$text":{
        "$search":"java -js" #只要$text中的两个键值有js的那么不会查询
   }
})
#这样,就通过了
MongoDB的特色:在进行全文检索的时候,还可以使用相似度打分的机制来检验查询的结果
给相似度打分:
db.news.find(
   {
        "$text":{
            "$search":"java"
       }
   },
   {
       "score":{
           "$meta":"textScore"
       } 
   }
)
#使用这个,相当于查询时,会加上score字段(只在查询结果出现,表上并没有),之所以会出现
#是因为"$meta":"textScore"的作用,否则一般是当作条件的,或者报错(对于修改"$meta":"textScore"来说)
#这时就会出现相似值,根据相似算法得出的值(这里就不解释了)
#一般在一个字段里,只有一个单词相同一个,就是1.1,只有两个单词相同一个,就是0.75
#只有三个单词相同一个,就是0.667,只有四个单词相同一个,就是0.625
#两个字段只有一个单词都相同一个,就是2.2(两个字段都相同一个)
#后面的自己找到算法进行测试(可以百度,实在不行,自己推断,(●ˇ∀ˇ●))
分值越大,越相似,也可以进行打分的排序(降序)
db.news.find(
   {
        "$text":{
            "$search":"java"
       }
   },
   {
       "score":{
           "$meta":"textScore"
            } 
   }
).sort({ 
    "score":{
           "$meta":"textScore"  #是降序的,不算正数和负数的那个操作了
           #必须也要这样写,否则报错,因为已经设置了score(没有设置的话,则不会),那么这里也就需要同样的设置
   }
})
为所有字段设置全文索引(先删掉全部索引,再创建):
db.news.dropIndexes()
db.news.ensureIndex({"$**":"text"}) #$**代表全部字段(或者说键),当然若有字段被设置过全局索引,则会报错
#当然普通的索引则不会,基本只对全局索引进行检查
虽然很简单,但是尽量别用,因为"慢",就如java使用if-else会比for快,但是麻烦
因为for相当于嵌套很多if-else且也有其他程序执行,还有他们之间的转换也需要时间
地理信息索引:
2DSphere:球面索引
2D索引:摇一摇,大众点评,美团都是基于2D索引的,保存的信息都是坐标,坐标都是经纬度
初始化店铺集合:
db.shops.insert({'loc':[10,10]})
db.shops.insert({'loc':[11,11]})
db.shops.insert({'loc':[13,12]})
db.shops.insert({'loc':[50,14]})
db.shops.insert({'loc':[66,66]})
db.shops.insert({'loc':[110,119]})
db.shops.insert({'loc':[93,24]})
db.shops.insert({'loc':[99,54]})
db.shops.insert({'loc':[77,7]})
创建2D索引:
db.shops.ensureIndex({
    "loc":"2d" #基本只会操作[77,7]这样的数据,而单词的字符串或者数字,不会操作,好像与{}类型的值不共存
    #若有该索引,即加不了{}的数据,若有{}的数据,那么索引就加不了
})
#索引之间的查询是不同的,可以看看2d和text,好像2d是普通的样子,而text则是特殊的样子

#注意:不是2D,而是2d,不要搞错了,否则基本是会报错的
2D索引创建完成之后,我们就可以实现坐标的查询了,有两种方式:
/*
$near : 查询距离某个点距离最近的坐标点
$geoWithin : 查询某个形状(如三角形,圆形等等)内的坐标点
假设我的坐标是[11,11],查询离我最近的点
db.shops.find({
    "loc":{
        "$near":[11,11] //需要先有2d索引,否则报错,若有相同的坐标,那么则根据find查询的先后顺序显示
   }
})
*/

//注意:这些范围本身基本也是包括的,就如坐标系一样
结果会将集合中基本所有的点(也就是坐标)都返回(可能有些可以设置前100)
若是有很多数据,那么就太多了,我们要设置范围
设置查询范围,5个点内的(好像只是增加点,并没有减少点,所以操作不了减少距离)
db.shops.find({
    "loc":{
        "$near":[11,11],
        "$maxDistance":5 #即[x,y]中x或者y不能超过[11,11]对应位置的值的5个点距离(这里都是11)
        #即16可以,但出现17就不可以
   }
})
虽然支持最大的距离(比如上面的5),但是不支持最小距离(好像并没有这样的设置),但我们可以设置形状内查询
#矩形范围 $box : {"$geoWithin":{"$box":[[x1,y1],[x2,y2]]}
db.shops.find({
    "loc":{
        "$geoWithin":{
            "$box":[[9,9],[11,11]] #是进行补全的,而不是直接连接
            #正是由于图形,那么对应的集合的先后位置,不必很严谨
       }
   }
})
#圆形范围 $center : {"$geoWithin":{"$center":[[x1,y1],r]}
db.shops.find({
    "loc":{
        "$geoWithin":{
            "$center":[[9,9],2] 
            #该位置开始,半径为2的圆形,由此可知,对应的坐标是可以为小数的
            #实际上负数也可,可以参照坐标系
       }
   }
})
#多边形范围 $polygon : {"$polygon":[[x1,y1],[x2,y2],[x3,y3],...]}
db.shops.find({
    "loc":{
        "$geoWithin":{
            "$polygon":[[9,9],[12,15],[66,7]] 
            #相当于将这些点全部连接形成的图形(不交叉的,必须这样,否则也基本不会是多边形)
            #这里一看就是三角形
       }
   }
})
索引底层实现原理分析:
MongoDB 是文档型的数据库,是一种 NOSQL,它使用BSON 格式保存数据
比如之前我们的表可能有用户表、订单表等等,还要建立他们之间的外键关联关系
但是BSON就不一样了,这种形式更简单,通俗易懂
MongoDB使用B-树,实际上-不是减号,而是分割符号,所以也称为B树,或者叫做B减(-)树
所有节点都有Data域,只要找到指定索引就可以进行访问,无疑单次查询平均快于Mysql
Mysql作为一个关系型数据库,使用B+树,数据的关联性是非常强的,区间访问是常态
B+树由于数据全部存储在叶子节点,并且通过指针串在一起,这样就很容易的进行区间遍历甚至全部遍历
B-树是一种自平衡的搜索树,形式很简单:

83-MongoDB介绍_第7张图片

B-树的特点:
1:多路,非二叉树
2:每个节点既保存索引,又保存数据
3:搜索时相当于二分查找
B+树是B-树的变种:

83-MongoDB介绍_第8张图片

B+树的特点:
1:多路非二叉
2:只有叶子(白色部分)节点保存数据
3:搜索时相当于二分查找
4:增加了相邻接点的指向指针
从上面我们可以看出最核心的区别主要有俩:
一个是数据的保存位置:B树保存在所有的节点中,B+树保存在叶子节点
一个是相邻节点的指向:B树叶子节点之间没有指针,B+树有
就是这俩造成他们的差别:
1:B+树查询时间复杂度固定是 O(logn),B-树查询复杂度最好是 O(1)
2:B+树相邻接点的指针可以大大增加区间遍历性,而B-树每个节点 key 和 data 在一起,遍历不方便
3:B+树更适合外部存储,也就是磁盘存储。由于内节点无 data 域,每个节点能索引的范围更大更精确
4:注意这个区别相当重要,是基于1,2,3的,B树每个节点即保存数据又保存索引
所以磁盘IO的次数很少,B+树只有叶子节点保存,磁盘IO多,但是区间访问比较好
总结:
1:区间访问是关系型数据库的法宝(因为有关联),所以要使用B+树
2:单一查询mongo用的最多,所以使用B-树
为什么关系型数据库遍历访问较多,而mongo单一访问较多呢:
mysql经常做表关联查询,而单表查询是不常见的,商品->订单->库存->用户等等多级关联,是家常便饭
mongo虽然有也lookup类似join的关联查询,但基本不用
在MongoDB中,根本不推荐这么设计数据关联,反之,如果你非要设计成mysql的数据关联
那么,你就用错mongo了,还是放弃mongo,选择mysql吧
因为mongo在数据关联(分开的,分开的数据关联也就是mysql数据关联)这里比不上mysql
这是数据结构的底层原因
用mongo如何设计数据关联呢 ,看下面的代码:
db.class.insert(
   {
        "id":1,
        "className":"一年一班",
        "students":[
           {"name":"吕布", "gender":"男"},
           {"name":"貂蝉", "gender":"女"}
       ]
 },
   {
        "id":2,
        "className":"一年二班",
        "students":[
           {"name":"韩立", "gender":"男"},
           {"name":"南宫婉", "gender":"女"}
       ]
 },
)
#我们将一个数据里面,直接关联,而不分开关联(mysql),使用自己的长处即可
#实际上可以这样的理解:我们将mysql的各个表的数据,集中一起,放在一个表中,这样的也就是mongo了,这是一种解释
索引的限制:
/*
1. 如果MongoDB的索引项(也就是键值)超过索引限制,即1024 bytes,将不会创建该索引(创建失败)
并且插入数据和更新数据会报错
2. 针对分片的collections,当数据迁移时,如果数据块中包含索引
属性超过了索引限制数据块的迁移将会失败
3. 一个collections最多能够有64个索引
4. 针对索引的全名,包含命名空间和"."分隔符,如: ..$ 
最多不超过128 characters
5. 针对复合索引,包含的索引属性不能够超过31个属性
6. 查询不能够同时使用文本索引和地理空间索引(Queries cannot use both text and Geospatial Indexes)
因为他们之间的值,并不能相融,一个只能操作文本(字符串,数字),一个只能操作数组,所有一起操作时,会报错
7. 包含2d sphere属性的索引,只能够针对地理空间属性

注意:
1.java中的方法名、变量名的最大长度可以为2的16次方个字符,换算后大概也就是64KB的大小
2.也就是说大概最大可以为64KB,超过这个长度后java文件就无法被编译了
3.但一般是见名知意,不超过20个字符

上面的限制被超过后,一般都会报错
/*
聚合:
MongoDB的产生,是依靠着大数据时代的到来
所谓的大数据,其实就进行数据的抓取,收集,汇总,这样就产生信息的统计操作,这就叫做"聚合",也就是"分组统计"
集合的数据量 :
集合中数据量
db.student.count() 
#返回集合里面的数据个数,前面好像操作了student集合
#若自己没有,可以自己创建一个,并加上数据
模糊查询之后的数据量,包含"孙":
db.student.count({"name":/孙/i}) #包含孙,且忽略大小写的该集合数据个数(虽然该忽略大小写并没有起作用)
查询全部也是模糊查询的一种,只不过是没有条件的模糊查询
没有条件的查询,永远比条件查询快很多
消除重复数据:
沿用了oracle的关键字,Distinct
没有直接的函数支持(没有集合.方法),所以我们只能用最原始的runCommand()
查询所有name,消除掉重复的name
db.student.find() #先看看具体数据
db.runCommand({
    "distinct":"student",
    "key":"name"
    #消除了student集合中,重复的name的数据,使用最前面的
    #注意,只是消除,不是删除,这是一种查询方式,且没有name字段的,不会查询
})
MapReduce :
Map:数据映射(数据取出,获取,来源)
Reduce:归约(数据处理)
可以理解为数据聚集一起,统计,找出需要的各种结果,可以百度百科"MapReduce",了解更深层次的内容
更简单的理解:select xxxx + group by
初始化集合数据,员工集合
db.emps.insert({"name":"张三","sex":"男","age":19,"job":"程序猿","salary":5000})
db.emps.insert({"name":"李四","sex":"女","age":18,"job":"美工","salary":6000})
db.emps.insert({"name":"王五","sex":"女","age":21,"job":"测试","salary":7000})
db.emps.insert({"name":"赵六","sex":"女","age":21,"job":"程猿","salary":5500})
db.emps.insert({"name":"孙七","sex":"男","age":22,"job":"测试","salary":8000})
db.emps.insert({"name":"田八","sex":"女","age":22,"job":"程序猿","salary":3000})
db.emps.insert({"name":"钱九","sex":"男","age":23,"job":"美工","salary":4500})
db.emps.insert({"name":"周十","sex":"男","age":17,"job":"程序猿","salary":9000})
需求:按照职位分组,取出每个职位的人名
编写分组定义:
var jobMap = function(){
 emit(this.job , this.name) 
 #按照job分组,取出name,其中使用谁分组,那么查询时,_id的值就是该分组的信息,比如_id是程序猿
}
#其中一组{key:"程序猿",values:[name,name,...]} 分好组后,对应的key一般都有多个value,有多个组
#编写reduce操作
var jobReduce = function(key,values){ 
#这个参数的名称,好像并没有做要求,即keye,或者valuese也可以
#随便写的两个参数(首先需要先符合变量名称的书写)
 return {job:key,names:values}; 
 #接受参数(jobMap每组的key和value),返回对应的新的{}值,用来放入新的集合中的
}
#mapReduce进行上面的操作,使得进行放入新的集合中的
#上面只是定义,真正的操作还是看这里
db.runCommand({
    "mapreduce":"emps",    
    #操作emps集合
    "map":jobMap, 
    #定义map,使得该方法里,操作的变量是emps集合的
    "reduce":jobReduce, 
    #定义reduce,会将得到的jobMap的key和value(多个,有多组)当成参数
    #并将返回值结果给下面的emps_job_names集合在
    "out":"emps_job_names"      
    #将统计的结果输出到这里的emps_job_names集合中
})
#执行之后,所有数据都保存在一个新的集合emps_job_names中了(对应的key和value的所有组)
db.emps_job_names.find() #进行查询,创建了一个集合了,因为上面的操作(第三步)会创建集合


#记得全部选中一起执行,其中,若多次的执行,上面的第三步会相当于覆盖
结果:

83-MongoDB介绍_第9张图片

发现,的确是每组的key-value放入了这里,总共有四组,每组都有多个value
这里就是按照job分组,分成了4个,每个组都有对应的name当成一个字段,也就是name
其中第3组,只有一条数据(这里就不展开了),也就是说,就算没有分组之前,也是没有其他相同的job的
案例:统计出男女的人数,平均工资,最高工资,最低工资,员工姓名
sql中几乎写不出来(因为有员工姓名的存在,使得非常麻烦,且分组一般只能操作一次)
即基本只能存储过程(其中操作多个姓名放在一起,比如使用 “,” 分开等等)或重名调用(当成一个结果表,进行关联等等)
接下来我们通过Mongo来搞定(因为他的键值可以是数组)
var sexMap = function(){
    #定义分组的条件,以及每个集合要取出的内容
    emit(this.sex,{
        "ccount":1,
        "csal":this.salary,
        "cavg":this.salary,
        "cmax":this.salary,
        "cmin":this.salary,
        "cname":this.name
   })
}

var sexReduce = function(key , values){
    var total = 0 #总数
    var sum = 0 #工资和
    var max = values[0].cmax #这里默认设置第一个为最高工资,后面会对第一个进行判断操作,使得得到最高工资
    var min = values[0].cmin #这里默认设置第一个为最低工资,后面会对第一个进行判断操作,使得得到最低工资
    var names = new Array() #定义姓名数组,这个就是与mysql的主要区别
    for(var i in values){ #该i是下标,而不是对应数组值
        total += values[i].ccount #人数++
        sum += values[i].csal #累加工资和
        
        if(max < values[i].cmax){
            max = values[i].cmax  #最高工资
       }
        
        if(min > values[i].cmin){
            min = values[i].cmin  #最低工资
       }
        
        names[i] = values[i].cname #保存姓名
   }
    
    var avg = (sum/total).toFixed(2) #平均工资,toFixed(2)保留两位小数
    
    return {
    "count":total,
        "avg":avg,
        "sum":sum,
        "max":max,
        "min":min,
        "names":names
   }
}

db.runCommand({
    "mapreduce":"emps",
    "map":sexMap,
    "reduce":sexReduce,
    "out":"emps_info"
})

db.emps_info.find()

#有些时候,运行前报红,并不是的代码的原因,实际上是可以执行的(可能检查的问题使得报红,可以百度进行解决)
#这里因为代码表类型,所有注释就是#了

#记得全部选中一起执行
聚合框架:
MapReduce虽然强大,但是写起来的复杂度也是相当的高
Mongo2.x之后,提供了聚合框架函数:aggregate()
$group:
分组:
每个职位的人数 $sum每次累加1
db.emps.aggregate({
    "$group":{
        "_id":"$job", 
        #该属性好像必须是_id,属性值一般是对应的字段key
        #若没有$,那么查询的结果中代表_id的值是该值,且job_count代表总数
        #若有$,那么就是根据后面的字段进行分组(若不是字段,那么就是null,且会使得$sum的值为16)
        #其中$sum使得分组显示对应的总人数
        #若设置1,代表每个人数加1,从0开始加,若设置2,那么就是每个人数加2,若不是数字,默认为0
        #可以自己测试设置
        
        "job_count":{
            "$sum":1 
       }
   }
})
每个职位的总工资:
db.emps.aggregate({
    "$group":{
        "_id":"$job",
        "job_salary_sum":{
            "$sum":"$salary" 
            #使用"$salary",由于加上了$,那么就不会是默认为0,而是对应分组中的各个人数对应的salary字段值
            #这里就是变化的了,而不是固定的1或者2的累加了
       }
   }
})
每个职位的总工资,平均工资,最高工资,最低工资:
db.emps.aggregate({
    "$group":{
        "_id":"$job",
        "sum":{
            "$sum":"$salary"
       },
        "avg":{
            "$avg":"$salary" #当操作人数操作完后,将对应的值全部相加,然后进行算平均值
       },
        "max":{
            "$max":"$salary" #依次比较,然后得出最大值
       },
        "min":{
            "$min":"$salary" #依次比较,然后得出最小值
       },
       }
})
每个职位的工资数据(使用$push,以数组的形式显示每个职工的工资)
db.emps.aggregate({
    "$group":{
        "_id":"$job",
        "salary":{
            "$push":"$salary" #将所有的对应salary字段值进行存放在一个数组里面,并给出结果
       }
   }
})
每个职位的员工姓名:
db.emps.aggregate({
    "$group":{
        "_id":"$job",
        "xingming":{
            "$push":"$name"
       }
   }
})
使用$push,可以数组显示,但是会出现重复,我们使用一个关键字来取消重复
取消员工姓名的重复
db.emps.aggregate({
    "$group":{
        "_id":"$job",
        "xingming":{
            "$addToSet":"$name" 
            #使得数组中相同的名称不重复,应该是优先小的,因为是将从小到大,变成了从大到小的数组,后面小的覆盖
       }
   }
})
$project:
数据列的显示规则,投影查询
1 | true 显示
0 | false不显示
不显示_id,显示name
一般没有指定条件的,基本都是查询所有,即操作所有
db.emps.aggregate({
    "$project":{
        "_id":0, #默认的会显示_id
        "name":1 #若是字符串,那么对应的就不单独是显示了
        #且无论是否有该字段,且显示后,会加上该key(不是字符串的是直接取消显示的)
        #且值是该字符串的值(全部)
        #这里与普通的投影不同,因为是$project的作用,普通的投影,只要不是0,一般都是显示,而不会设置值
        #但无论是什么投影,基本都是查询的操作,实际上一般的函数都是跟查询有关系的,很少有操作集合的
        #通常跟操作key和操作value的区别有关
   }
   #使用这个$project,那么必须写字段,否则报错
   #且其他的字段都默认为0(_id除外,是默认为1的),而不是显示了,可以说不是默认为1
   #即不显示
})
起别名显示:
db.emps.aggregate({
    "$project":{
        "_id":0,
        "姓名":"$name", 
        "职位":"$job",
        "工资":"$salary"
        #加上$后面跟上字段,那么就是当前的这条数据的该字段值(可以看成替换)
        #比如"5000",而不是5000(因为这里若不是字符串就不显示了)
        #但是若加上$且没有对应字段,那么不显示
   }
})
聚合管道运算符中文文档:
https://www.docs4dev.com/docs/zh/mongodb/v3.6/reference/reference-operator-aggregation-abs.html
支持四则运算
加 $add
减 $subtract
乘 $multiply
除 $divide
求模 $mod
求年薪:
db.emps.aggregate({
    "$project":{
        "_id":0,
        "姓名":"$name",
        "职位":"$job",
        "工资":"$salary",
        "年薪":{
            "$multiply":["$salary",12] 
            #我们发现,在对一个key的值操作时,一般会使得key值变成{}
            #这是底层的获取方式,使得操作对应的$的操作进行了
            #大多数情况下都是这样,可以看看前面的操作就知道了,而一般的对key的操作,如删除,都是包括key
            #这里就是操作$salary进行替换,然后乘12,当然实际上也可以自己写固定的,因为是替换
            #进行返回给key(无论key是有无存在字段,因为返回的是字符串,即字符串数字或者字符串)
            #一般$的操作返回的一般都是字符串
            #最后显示给我们看
            
            #一般$都是进行替换字段值,除了特殊的语法
       }
   }
})
支持关系运算:
大小比较 $cmp
等于 $eq
大于 $gt
大于等于 $gte
小于 $lt
小于等于 $lte
不等于 $ne
判断null $ifNull
支持逻辑运算:
与 $and
或 $or
非 $not
字符串操作:
连接 $concat
截取 $substr
小写 $toLower
大写 $toUpper
忽略大小写比较 $strcasecmp
找出工资大于等于8000的员工姓名,工资
db.emps.aggregate({
    "$project":{
        "_id":0,
        "姓名":"$name",
        "职位":"$job",
        "工资":"$salary",
        "money":{
            "$gte":["$salary",8000] #大于等于8000的显示,返回"true"
       }
   }
})
查询职位是"程序猿"
正常比较
db.emps.aggregate({
    "$project":{
        "_id":0,
        "姓名":"$name",
        "职位":"$job",
        "is程序猿":{
            "$eq":["$job","程序猿"] #是就返回true
       }
   }
})
转成大写比较:
db.emps.aggregate({
    "$project":{
        "_id":0,
        "姓名":"$name",
        "职位":"$job",
        "is程序猿":{
            "$eq":["$job",{"$toUpper":"程序猿"}] #变成大写,进行比较,虽然并没有什么作用
       }
   }
})
忽略大小写比较:
db.emps.aggregate({
    "$project":{
        "_id":0,
        "姓名":"$name",
        "职位":"$job",
        "is程序猿":{
            "$strcasecmp":["$job","程序猿"] 
       }
   }
})
#返回值有0,1,-1
#0 : 相等(基本上unicode相等)
#1 : unicode靠后,例如,在unicode编码中,m>c,意味着m的编码要在c编码的后面
#-1 : unicode靠前 
#实际上也是可以看成ASCII的位置进行比较的
截取字符串第一个字:
在操作之前,需要先注意以下解释:
运算符使用 UTF-8 编码字节的索引,其中每个代码点或字符都可以使用一到四个字节进行编码
US-ASCII (英文字母)字符使用一个字节编码
带有变音符号的字符和其他拉丁字母字符(即英语字母之外的拉丁字符)使用两个字节进行编码
中文,日文和韩 Literals 符通常需要三个字节,而其他 Unicode 平面(表情符号,math 符号等)则需要四个字节
db.emps.aggregate({
    "$project":{
        "_id":0,
        "姓名":"$name",
        "职位":"$job",
        "姓氏":{
            "$substrBytes":["$name",0,3]
            
            #其中$name代表需要操作的字符串,一般我们自己使用$变量,前面的操作基本都是如此,而不是自己写的固定
            #0代表开始位置,3代表结束位置,这里就需要上面的解释了
            #由于对应的替换后是汉字,那么每个汉字都是3个字节,即使用$substrBytes时,对应的0,和3代表字节位置
            #假如$name的值是李四,那么就是6个自己,这里就假设为1,2,3,4,5,6这些位置
            #其中0代表1这个位置,3代表4这个位置,包括1,不包括4,即取出1,2,3
            #由于1,2,3正好是一个汉字(3字节),那么返回成功不报错,即返回李
            #假设我们的0,3是1,4,取出了2,3,4,那么由于不是一个汉字,就会报错
            #所以,起始位置一般是3的相加,即0,3,6等等,而结束位置一般也要是3的相加,如3,6,9等等
            #因为我们取出的需要是完整的汉字,而不是部分,若是部分,那么就会报错
		   #当然,若结束超过了总位置,那么就是全部的截取
		   #如0,100,那么就是1,2,3,4,5,6,再怎么设置大的结束位置,也是如此
       }
   }
})
$sort :
1:年龄升序
db.emps.aggregate({
    "$sort":{
        "age":1 #1升序,-1降序,只能是1和-1,其他都基本报错
   }
   #需要存在key,否则报错
})
-1:工资降序
db.emps.aggregate({
    "$sort":{
        "salary":-1
   }
})
年龄升序,如果年龄相等(相等自然按照从上到下的位置排列,一般是老的在前,基本都是如此,虽然直接查看的是新的在前)
则工资降序排列,反之亦然,都相等,就按照从上到下的位置排列
db.emps.aggregate({
    "$sort":{
        "age":1,
        "salary":-1
   }
})
一般没指定排序的,查询基本都是从老到新的显示
$分页处理:
$limit:取出个数
$skip:跨过个数
取出3个值
db.emps.aggregate(
   {
        "$project":{
            "_id":0,
            "name":1,
            "age":1,
            "job":1
       }
   },
   {
        "$limit":3 #只取出前三个,显示,这里是aggregate方法的第二个参数({})
        
   }
)
跨过3行数据 (先跨,再取)
db.emps.aggregate(
   {
        "$project":{
            "_id":0,
            "name":1,
            "age":1,
            "job":1
       }
   },
   {
        "$skip":3 
        
   },
   {
        "$limit":3
        
   }
   #其中他们的顺序不同,会出现不同的结果(与普通的分页不同)
   #普通的分页是一起操作,这里是分开操作,这就会使得出现如下情况
   #假设有五条数据,如1,2,3,4,5
   #普通的分页虽然跨条数,和指定条数可以互相调用
   #但最终的结果还是先跨在指定(因为一起操作的,并没有局部的分开)
   #而这里就有区别,由于是局部的执行,那么若是$skip在$limit前,那么没有问题,相当于先跨然后指定
   #若是$skip在$limit后,那么就会出现问题
   #也就是先取数据(这时一般会默认跨0,即从下标0开始,即从跨开始),然后进行跨(实际上也就是下标的指定)
   #根据这里来说,那么就是先取出1,2,3这三条,由于跨是下标
   #一般我们的结果是根据跨的下标和指定条数的最终位置来决定分页的一页显示
   #所以我们的下标为3时,那么也就是指向4,由于先后(4到不了最终的3)都取不到,那么就是返回空的
   #若这时将$skip修改成2,那么正好开始位置与结束位置都是2,即取出了2这个数据,自己可以进行测试
   #所有这里就需要注意一下先后位置
)
$unwind:
查询数据的时候,会返回数组信息,数组不方便浏览
我们要将数组变成独立的字符串
初始数据
db.dept.insert({"name":"技术部","业务":["研发","培训","维护"]})
db.dept.insert({"name":"人事部","业务":["招聘","调岗","发工资"]})
db.dept.insert({"name":"市场部","业务":["销售","渠道","开拓"]})
转换:
db.dept.aggregate([{"$unwind":"$业务"}])
#返回9条数据,为什么:
#因为从开始来说(老到新),那么从业务是"研发"开始,将他当作一条数据,其他字段也加入该数据里面,形成了9条数据
#部分的显示如下:

83-MongoDB介绍_第10张图片

发现的确如此
$out :
将查询结果输出到指定集合中,类似mapreduce
将投影结果,输出到一个集合中
db.emps.aggregate(
   {
        "$project":{
            "_id":0,
            "name":1,
            "job":1
       }
   },
   {
        "$skip":3
   },
   {
        "$limit":3
   },
   {
   "$out":"emps_test" 
   #将结果的集合数据,放入该集合里(顺便创建,若有则覆盖)
   #实际上覆盖也可以理解为,先删除再添加,大多数都可以这样说
   }
)
db.emps_test.find()
$geoNear :
得到附近的坐标点
帮助文档:
https://docs.mongodb.com/manual/reference/operator/aggregation/geoNear/#mongodb-pipeline-pipe.-geoNear
初始化数据
db.shops.drop()
db.shops.insert({'loc':[10,10]})
db.shops.insert({'loc':[11,11]})
db.shops.insert({'loc':[13,12]})
db.shops.insert({'loc':[50,14]})
db.shops.insert({'loc':[66,66]})
db.shops.insert({'loc':[110,119]})
db.shops.insert({'loc':[93,24]})
db.shops.insert({'loc':[99,54]})
db.shops.insert({'loc':[77,7]})
得到附近的坐标点
必须先建立2D索引,否则查询报错
db.shops.ensureIndex({
    "loc":"2d"
})
db.shops.aggregate({
"$geoNear":{
        "near":[11,11],    #坐标点
        "distanceField":"loc",   #操作的字段
        "maxDistance":2,   #最远距离,并不是按照点来的(点,直接加),好像是按照弧度数来的,可以看帮助文档
        "spherical":true    #按照球面查询
   }
},{
    "$limit":5    #返回坐标的数量
})
$lookup :
多表关联(3.2版本新增)主要功能:
是将每个输入待处理的文档,经过$lookup 阶段的处理
输出的新文档中会包含一个新生成的数组列(户名可根据需要命名新key的名字 )
数组列存放的数据 是 来自 被Join 集合的适配文档,如果没有,集合为空(即 为[ ])
基本语法:
{
 $lookup:
 {
 from: <collection to join> 从表,
 localField: <field from the input documents> 主表主键,
 foreignField: <field from the documents of the "from" collection> 从表主键,
 as: <output array field> 输出文档的新增值命名
 }
}
以上的语法介绍有些枯燥,不易理解,我们直接操作案例
订单集合,测试数据 如下:
db.orders.insert([
 { "order_id" : 1, "pname" : "可乐", "price" : 12, "count" : 2 },
 { "order_id" : 2, "pname" : "薯片", "price" : 20, "count" : 1 },
 { "order_id" : 3 }
])
#一次进行添加多个数据,而不用多次执行insert方法了
库存集合 ,测试数据 如下:
db.stock.insert([
 { "s_id" : 1, "sku" : "可乐", "description": "product 1", "instock" : 120 },
 { "s_id" : 2, "sku" : "面包", "description": "product 2", "instock" : 80 },
 { "s_id" : 3, "sku" : "香肠", "description": "product 3", "instock" : 60 },
 { "s_id" : 4, "sku" : "薯片", "description": "product 4", "instock" : 70 },
 { "s_id" : 5, "sku" : null,  "description": "Incomplete" },
 { "s_id" : 6 }
])
此集合中的 sku 等同于 订单 集合中的 pname
在这种模式设计下,如果要查询订单表对应商品的库存情况,应如何写代码呢
很明显这需要两个集合Join,场景简单,不做赘述,直送答案
如下:
db.orders.aggregate({ #这里操作方法的就相当于是主表(orders)
    "$lookup":{
        from: "stock",   #相当于从表
        localField: "pname", #相当于主表主键
        foreignField: "sku",  #相当于从表主键
        as: "shop_stock"  #新生的关联字段
   }
})
#新生的字段值,也只是再查询时出现,并没有真正的存在,且对应的值是数组
#数组里面存放了从表对应相同的两个指定主键的{}数据,若从表没有主表主键的字段或者该字段是null
#即从表主键是null或者没有的,则给对应的数组
#对于mysql来说,那就是都是null的效果,类似于left连接,虽然只有一个null,所有说类似
上述过程,其实和关系型数据库中的左外连接(left)查询非常相像(保证操作的orders集合数据完全)
固定集合:
规定集合大小,如果保存内容超过了集合的长度,那么会采用LRU的算法(最近最少使用的原则,或者是最老删除原则)
将最早(也就是最早加的,或者说是最老的)的数据移除,来保存新的数据,就是名将韩信管理粮仓的政策"推陈出新"
创建集合必须明确创建一个空集合
创建一个集合并设置集合参数
db.createCollection("depts",{
    "capped":true,  #true:固定集合
    "size":1024, #集合空间容量 1024字节
    "max":5 #最多有5条记录
})
初始化数据:
db.depts.insert({"name":"开发部-1","loc":"北京"})
db.depts.insert({"name":"开发部-2","loc":"北京"})
db.depts.insert({"name":"开发部-3","loc":"北京"})
db.depts.insert({"name":"开发部-4","loc":"北京"})
db.depts.insert({"name":"开发部-5","loc":"北京"})
再加入一条
db.depts.insert({"name":"开发部-6","loc":"北京"})
#发现,最老的数据,被删除了(对于5条封顶来说)
Java操作:
这里介绍spring提供对MongoDB操作的工具类的使用
对应的目录:

83-MongoDB介绍_第11张图片

对应的依赖:
<dependency>
    
    <groupId>org.springframework.datagroupId>
    <artifactId>spring-data-mongodbartifactId>
    <version>2.0.9.RELEASEversion>
dependency>
spring配置如下:在配置文件中配置 MongoTemplate (resources下创建application.xml)

<beans xmlns="http://www.springframework.org/schema/beans"
       xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
       xmlns:mongo="http://www.springframework.org/schema/data/mongo"
       xmlns:context="http://www.springframework.org/schema/context"
       xsi:schemaLocation="
        http://www.springframework.org/schema/beans
        http://www.springframework.org/schema/beans/springbeans.xsd
        http://www.springframework.org/schema/context
        http://www.springframework.org/schema/context/spring-context.xsd
        http://www.springframework.org/schema/data/mongo
        http://www.springframework.org/schema/data/mongo/spring-mongo.xsd">
    
    <mongo:db-factory id="mongoDbFactory" client-uri="mongodb://192.168.164.128:27017/laosun"/>

    <bean id="mongoTemplate"
          class="org.springframework.data.mongodb.core.MongoTemplate">
 
        <constructor-arg name="mongoDbFactory"
                         ref="mongoDbFactory">constructor-arg>
    bean>
    <context:component-scan base-package="dao">context:component-scan>
beans>

若打开idea的项目时,maven的仓库路径会改变,可以到启动的idea页面那里进行修改(这时大概是全局的)
对应的Emp类:
package entity;

import org.springframework.data.mongodb.core.mapping.Document;

import java.io.Serializable;

/**
 *
 */
@Document(collection = "emps") //指定对应的集合,没有该集合,自然也是自动创建
public class Emp implements Serializable {
    private String _id;
    private String name;
    private String sex;
    private double age;
    private String job;
    private double salary;
    
    @Override
    public String toString() {
        return "Emp{" +
                "_id='" + _id + '\'' +
                ", name='" + name + '\'' +
                ", sex='" + sex + '\'' +
                ", age=" + age +
                ", job='" + job + '\'' +
                ", salary=" + salary +
                '}';
    }

    public Emp() {
    }

    //定义一个没有_id,的因为会自动生成,如果不要自动生成的
    //那么可以使用下面的全部参数的构造方法,但无论怎么搞,_id都是默认有唯一索引的(虽然不显示给你看)
    public Emp(String name, String sex, double age, String job, double salary) {
        this.name = name;
        this.sex = sex;
        this.age = age;
        this.job = job;
        this.salary = salary;
    }

    public Emp(String _id, String name, String sex, double age, String job, double salary) {
        this._id = _id;
        this.name = name;
        this.sex = sex;
        this.age = age;
        this.job = job;
        this.salary = salary;
    }


    public String get_id() {
        return _id;
    }

    public void set_id(String _id) {
        this._id = _id;
    }

    public String getName() {
        return name;
    }

    public void setName(String name) {
        this.name = name;
    }

    public String getSex() {
        return sex;
    }

    public void setSex(String sex) {
        this.sex = sex;
    }

    public double getAge() {
        return age;
    }

    public void setAge(double age) {
        this.age = age;
    }

    public String getJob() {
        return job;
    }

    public void setJob(String job) {
        this.job = job;
    }

    public double getSalary() {
        return salary;
    }

    public void setSalary(double salary) {
        this.salary = salary;
    }
}

对应的EmpDao接口及其实现类:
package dao;

import entity.Emp;

import java.util.List;

/**
 *
 */
public interface EmpDao {

    void save(Emp emp);

    void delete(String id);

    void update(Emp emp);
    
    Emp findById(String id);

    List<Emp> findListPage(Integer pageIndex,Integer pageSize,String name);
}

操作实现类之前,首先先介绍对应的Mongo使用类的方法:
常用方法:
//查询Emp文档的全部数据
mongoTemplate.findAll(Emp.class) 
//根据id进行查询(该id表示_id的值),Emp.class指定对应集合位置(一般是根据注解)
mongoTemplate.findById(Object id, Emp.class) 
//根据query内的查询条件查询
mongoTemplate.find(Query query, Emp.class) 
//修改操作
//第一个参数,可以说是原来的数据位置(可以理解为查询得到)
//第二个参数可以说修改的对应值,一般指定key和value
//第三个参数,使得前面两个参数去对应的位置(也就是集合)进行查询修改操作
mongoTemplate.upsert(Query query, Update update, Emp.class) 
//删除操作
//第一个参数,可以说是原来的数据位置(可以理解为查询得到)
//第二个参数,使得前面的参数去对应的位置进行查询删除操作
mongoTemplate.remove(Query query, Emp.class) 
//新增操作,Emp里面的变量根据key-value格式进行添加,key是变量,value是值,当然也包括类型
mongoTemplate.insert(Object Emp) 
Query对象:
/*
创建一个query对象(用来封装所有条件对象),再创建一个criteria对象(用来构建条件)
精准条件:criteria.and("key").is("1(条件)") 
and相当于加条件(给criteria加条件,而不是Query),一般需要有初始条件,如先有where,否则操作不了
相当于 where 1=1 and key = 1 从and开始,当然and可以直接修改成where,省略了1=1
模糊条件:criteria.and("key").regex("条件")
封装条件:query.addCriteria(criteria)
大于(创建新的criteria):Criteria gt = Criteria.where("key").gt("条件")
小于(创建新的criteria):Criteria lt = Criteria.where("key").lt("条件")
Query.addCriteria(new Criteria().andOperator(gt,lt))
给Query加上条件(使得我们创建无参Query对象时,可以进行补充条件)
一个query中只能有一个andOperator(),其参数也可以是Criteria数组
排序 :query.with(new Sort(Sort.Direction.ASC, "age").and(new Sort(Sort.Direction.DESC, "date")))
*/
对应的实现类:
package dao.impl;

import dao.EmpDao;
import entity.Emp;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.mongodb.core.MongoTemplate;
import org.springframework.stereotype.Repository;

import java.util.List;

/**
 *
 */
@Repository("empDao")
public class EmpDaoImpl implements EmpDao {

    @Autowired
    private MongoTemplate mongoTemplate;

    @Override
    public void save(Emp emp) {
        
        //新增操作,Emp里面的变量根据key-value格式进行添加,key是变量,value是值,当然也包括类型
        mongoTemplate.insert(emp);

    }

    @Override
    public void delete(String id) {

    }

    @Override
    public void update(Emp emp) {

    }
    
    @Override
    public Emp findById(String id) {

    }

    @Override
    public List<Emp> findListPage(Integer pageIndex, Integer pageSize, String name) {
        return null;
    }
}

测试TestMongo类:
package test;

import dao.EmpDao;
import entity.Emp;
import org.springframework.context.support.ClassPathXmlApplicationContext;

/**
 *
 */
public class TestMongo {
    public static void main(String[] args) {
        ClassPathXmlApplicationContext context = new 
            ClassPathXmlApplicationContext("application.xml");
        EmpDao empDao = context.getBean("empDao", EmpDao.class);
        Emp emp = new Emp("1", "1", 1, "1", 1); 
        //该类指定了集合,因为虽然扫描时,没有扫描
        //但是下面的操作会根据该注解进行指定集合的存放,因为注解也是类的一部分
        empDao.save(emp); //当我们创建时,根据数据库并根据集合,进行数据的添加
        System.out.println("添加成功");
        
        //这样我们只需要操作数据即可,因为数据库和集合都指定了
    }
}

执行成功,可以看看对应的数据库是否有数据了,若有数据,则完成了添加操作,接下来进行扩展实现类
实现修改,删除,查询:
扩展的实现类EmpDaoImpl:
package dao.impl;

import com.mongodb.client.result.DeleteResult;
import com.mongodb.client.result.UpdateResult;
import dao.EmpDao;
import entity.Emp;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.mongodb.core.MongoTemplate;
import org.springframework.data.mongodb.core.query.Criteria;
import org.springframework.data.mongodb.core.query.Query;
import org.springframework.data.mongodb.core.query.Update;
import org.springframework.stereotype.Repository;
import org.springframework.util.StringUtils;

import java.util.Collection;
import java.util.Iterator;
import java.util.List;
import java.util.Queue;
import java.util.regex.Pattern;

/**
 *
 */
@Repository("empDao")
public class EmpDaoImpl implements EmpDao {

    @Autowired
    private MongoTemplate mongoTemplate;

    @Override
    public void save(Emp emp) {

        //新增操作,Emp里面的变量根据key-value格式进行添加,key是变量,value是值,当然也包括类型
        mongoTemplate.insert(emp);

    }

    @Override
    public void delete(String id) {
        Query query = new Query(Criteria.where("_id").is(id));
        //删除操作
        //第一个参数,可以说是原来的数据位置(可以理解为查询得到)
        //第二个参数,使得前面的参数去对应的位置进行查询删除操作
        DeleteResult remove = mongoTemplate.remove(query, Emp.class);
        //被删除的数量
        long deletedCount = remove.getDeletedCount();
        if (deletedCount > 0){
            System.out.println("删除成功");
        }else{
            System.out.println("删除失败");
        }

    }

    @Override
    public void update(Emp emp) {
        //记得导入这个import org.springframework.data.mongodb.core.query.Query;

        //查询被修改的对象
        Query query = new Query(Criteria.where("_id").is(emp.get_id()));

        //设置全新的数据对象
        Update update = new Update();
        update.set("name",emp.getName());
        update.set("age",emp.getAge());
        update.set("sex",emp.getSex());
        update.set("job",emp.getJob());
        update.set("salary",emp.getSalary());
        //上面只是定义,若需要真的操作,需要对应的集合
        //即需要Emp.class来指定对应的集合,从而根据这些定义进行操作
        //修改操作
        //第一个参数,可以说是原来的数据位置(可以理解为查询得到)
        //第二个参数可以说修改的对应值,一般指定key和value
        //第三个参数,使得前面两个参数去对应的位置(也就是集合)进行查询修改操作
        UpdateResult upsert = mongoTemplate.upsert(query, update, Emp.class);
        //得到修改的数量,相同的也是修改,只是值一样而已
        long modifiedCount = upsert.getModifiedCount();
        if(modifiedCount > 0){
            System.out.println("修改成功");
        }else{
            System.out.println("修改失败");
        }

    }

    @Override
    public Emp findById(String id) {
        //根据id进行查询(该id表示_id的值),Emp.class指定对应集合位置
        Emp byId = mongoTemplate.findById(id, Emp.class);
        return byId;

    }

    @Override
    public List<Emp> findListPage(Integer pageIndex, Integer pageSize, String name) {

        Query query = new Query();

        //StringUtils.isEmpty()方法封装了null和""的判断操作
        //只要是他们其中一个,就会返回true,所有这里加上了!
        if(!StringUtils.isEmpty(name)) {
            // 分页查询(模糊)
            // 模糊查询以 【^】开始 以【$】结束 【.*】相当于Mysql中的%
            //使用正则拼装模糊查询的条件
            //实际上就算name是null,那么%s也会将null当成结果
            //即^.*null.*$,那么只要是null那么就可以查询到
            //若是"",那么只要有name的,一般都会查询出来
            String format = String.format("%s%s%s", "^.*", name, ".*$");

            //我们可以先不传递正则字符串,可以先进行设置模糊的参数,使得匹配时会操作该参数
            //构建正则条件对象,Pattern.CASE_INSENSITIVE忽略正则中的大小写
            Pattern compile = Pattern.compile(format, Pattern.CASE_INSENSITIVE);


            query.addCriteria(Criteria.where("name").regex(compile));

        }

        //获取数据条数,这里是获取总数量(无条件,那么基本就是总数量了)
        long count = mongoTemplate.count(query, Emp.class);
        System.out.println(count);

        //分页,跳过(pageIndex-1) * pageSize条
        //因为我们传递的是页数,需要得到对应的每页的最开始的下标,来实现分页的具体显示,然后返回pageSize条
        List<Emp> emps = mongoTemplate.find(query.skip((pageIndex-1) * pageSize).limit(pageSize), 
                                            Emp.class);
        return emps;
    }
}

对应的扩展测试类TestMongo:
package test;

import dao.EmpDao;
import entity.Emp;
import org.springframework.context.support.ClassPathXmlApplicationContext;

import java.util.List;

/**
 *
 */
public class TestMongo {
    public static void main(String[] args) {
        ClassPathXmlApplicationContext context = new 
            ClassPathXmlApplicationContext("application.xml");
        EmpDao empDao = context.getBean("empDao", EmpDao.class);
        /*
        Emp emp = new Emp("1", "2", 1, "1", 1);
        empDao.save(emp);
        System.out.println("添加成功");
         */

        /*
        这个_id是里面的值,假如加上了前面的ObjectId
        是会修改失败的(因为_id不同,且他会默认加上ObjectId进行包括,也有可能不会加上,可能只对他来说)
        Emp emp = new Emp("62c64ace36973c217c392dcd","吕布","男",20,"哈哈",1000);
        empDao.update(emp);
         */

        /*
        也会默认加上ObjectId进行包括,也有可能不会加上(可能只对他来说)
        empDao.delete("62c64a3736973c1de4f75448");
         */

        /*
        也会默认加上ObjectId进行包括,也有可能不会加上(可能只对他来说)
        Emp byId = empDao.findById("62c6487b36973c36b811d7c0");
        System.out.println(byId);
         */

        //在有条件的基础下,将查询的结果,再次操作分页
        List<Emp> listPage = empDao.findListPage(1,5,"哈");
        System.out.println(listPage);
    }


}

执行,测试,至此,增删改查全部完成
上面使用了String.format()方法,我们最后,说明一下这个方法:
根据案列来进行操作:
创建test类,随便一个地方即可,因为只是进行测试的
package dao.impl;

/**
 *
 */
public class test {
    public static void main(String[] args) {
        String str=null;
        //参数1:该String的值
        //参数2:参数1中,识别的对应符号所对应的值,当然若有该符号,那么参数2需要存在,否则报错
        //实际上参数2是可变长参数
        //String.format("参数1","参数2")
        str=String.format("Hi,%s", "小超"); //%s代表这参数2中可变长参数的第一个值,
        System.out.println(str); //Hi,小超
        //当有多个%s时,也就代表这参数2中可变长参数的对应下标值,从0开始
        str=String.format("Hi,%s %s %s", "小超","是个","大帅哥");
        System.out.println(str); //Hi,小超 是个 大帅哥(中间的空格不会省略)
        //所有上面说的不存在就是下标没有对应值造成的报错
        System.out.printf("Hi,%s", "小超");
        //也相当于上面的String.format("Hi,%s", "小超"),只是这里是直接输出,并没有好的保存
        
        //所以,一般的,我们也可以使用String.format来操作字符串,这也是一种方式
        //使得更好的观察主体

    }
}

你可能感兴趣的:(笔记,mongodb)