mongodb学习笔记

一、数据库(database)

① 什么是数据库?
存储数据的仓库

②为什么要有数据库?
首先数据是存储在内存里运行的,如果一断电的话,数据就会丢失,所以可以将数据存储到硬盘,但是硬盘没有联网,所以有了可以联网也可以存储数据的数据库。

③还有什么像数据库一样的玩意?
暂未找到…

④数据库能做什么?
数据库可以存储数据,并且这些数据还可以连接网络,也就是和硬盘相比,可以不用将数据库到处带,就像是网盘。

⑤数据库的服务器和客户端
值得注意的是数据库它也是分为服务器和客户端的

    服务器:保存数据的
    客户端:操作和存储数据的(CRUD)

⑥数据库的分类
按照关系型分类:

1、关系型数据库(MySQL、Orcal等)
2、非关系型数据库(MongoDB)

关系型和非关系型的区别:
总而言之就是:关系型的是创建表格, 非关系型是可以创建任意多个文档。

1、数据存储方式不同。
关系型和非关系型数据库的主要差异是数据存储的方式。关系型数据天然就是表格式的,因此存储在数据表的行和列中。数据表可以彼此关联协作存储,也很容易提取数据。

与其相反,非关系型数据不适合存储在数据表的行和列中,而是大块组合在一起。非关系型数据通常存储在数据集中,就像文档、键值对或者图结构。你的数据及其特性是选择数据存储和提取方式的首要影响因素。

2、扩展方式不同。

SQL和NoSQL数据库最大的差别可能是在扩展方式上,要支持日益增长的需求当然要扩展。
要支持更多并发量,SQL数据库是纵向扩展,也就是说提高处理能力,使用速度更快速的计算机,这样处理相同的数据集就更快了。

因为数据存储在关系表中,操作的性能瓶颈可能涉及很多个表,这都需要通过提高计算机性能来客服。虽然SQL数据库有很大扩展空间,但最终肯定会达到纵向扩展的上限。而NoSQL数据库是横向扩展的。

而非关系型数据存储天然就是分布式的,NoSQL数据库的扩展可以通过给资源池添加更多普通的数据库服务器(节点)来分担负载。

3、对事务性的支持不同。

如果数据操作需要高事务性或者复杂数据查询需要控制执行计划,那么传统的SQL数据库从性能和稳定性方面考虑是你的最佳选择。SQL数据库支持对事务原子性细粒度控制,并且易于回滚事务。

二、MongoDB

①什么是MongoDB?

一种数据库,而且是非关系型数据库。

MongoDB是一个开源、高性能、无模式的文档型数据库,当初的设计就是用于简化开发和方便扩展,是NoSQL数据库产品中的一种。是最像关系型数据库(MySQL)的非关系型数据库。

它支持的数据结构非常松散,是一种类似于 JSON 的 格式叫BSON,所以它既可以存储比较复杂的数据类型,又相当的灵活。

MongoDB中的记录是一个文档,它是一个由字段和值对(fifield:value)组成的数据结构。MongoDB文档类似于JSON对象,即一个文档认 为就是一个对象。字段的数据类型是字符型,它的值除了使用基本的一些类型外,还可以包括其他文档、普通数组和文档数组

 

体系结构

MySQL和MongoDB对比

 

 

术语

 

 

 相关术语对比

image

数据模型

MongoDB的最小存储单位就是文档(document)对象。文档(document)对象对应于关系型数据库的行。数据在MongoDB中以 BSON(Binary-JSON)文档的格式存储在磁盘上。

BSON(Binary Serialized Document Format)是一种类json的一种二进制形式的存储格式,简称Binary JSON。BSON和JSON一样,支持 内嵌的文档对象和数组对象,但是BSON有JSON没有的一些数据类型,如Date和BinData类型。

BSON采用了类似于 C 语言结构体的名称、对表示方法,支持内嵌的文档对象和数组对象,具有轻量性、可遍历性、高效性的三个特点,可 以有效描述非结构化数据和结构化数据。这种格式的优点是灵活性高,但它的缺点是空间利用率不是很理想。

Bson中,除了基本的JSON类型:string,integer,boolean,double,null,array和object,mongo还使用了特殊的数据类型。这些类型包括 date,object id,binary data,regular expression 和code。每一个驱动都以特定语言的方式实现了这些类型,查看你的驱动的文档来获取详 细信息。

BSON数据类型参考列表:

image

注意:
shell默认使用64位浮点型数值。{“x”:3.14}或{“x”:3}。对于整型值,可以使用NumberInt(4字节符号整数)或NumberLong(8字节符号整数),{“x”:NumberInt(“3”)}{“x”:NumberLong(“3”)}

查询中NumberLong类型需要显式指定:

db.qm_device_info.find({"_id": NumberLong("1330434141375111111")});

1.3 MongoDB的特点

MongoDB主要有如下特点:
(1)高性能
MongoDB提供高性能的数据持久性。特别是,对嵌入式数据模型的支持减少了数据库系统上的I/O活动。
索引支持更快的查询,并且可以包含来自嵌入式文档和数组的键。(文本索引解决搜索的需求、TTL索引解决历史数据自动过期的需求、地理位置索引可用于构建各种 O2O 应用)

mmapv1、wiredtiger、mongorocks(rocksdb)、in-memory 等多引擎支持满足各种场景需求。Gridfs解决文件存储的需求。
(2)高可用性
MongoDB的复制工具称为副本集(replica set),它可提供自动故障转移和数据冗余。
(3)高扩展性
MongoDB提供了水平可扩展性作为其核心功能的一部分。分片将数据分布在一组集群的机器上。(海量数据存储,服务能力水平扩展)从3.4开始,MongoDB支持基于片键创建数据区域。在一个平衡的集群中,MongoDB将一个区域所覆盖的读写只定向到该区域内的那些片。
(4)丰富的查询支持
MongoDB支持丰富的查询语言,支持读和写操作(CRUD),比如数据聚合、文本搜索和地理空间查询等。
(5)其他特点:如无模式(动态模式)、灵活的文档模型、

1.5 MongoDB业务应用场景

传统的关系型数据库(如MySQL),在数据操作的“三高”需求以及应对Web2.0的网站需求面前,显得力不从心。而MongoDB可应对“三高”需求。

“三高”需求:
• High performance - 对数据库高并发读写的需求。
• Huge Storage - 对海量数据的高效率存储和访问的需求。
• High Scalability && High Availability- 对数据库的高可扩展性和高可用性的需求。

MongoDB具体的比较常见的应用场景:

社交场景,使用 MongoDB 存储存储用户信息,以及用户发表的朋友圈信息,通过地理位置索引实现附近的人、地点等功能。
游戏场景,使用 MongoDB 存储游戏用户信息,用户的装备、积分等直接以内嵌文档的形式存储,方便查询、高效率存储和访问。
物流场景,使用 MongoDB 存储订单信息,订单状态在运送过程中会不断更新,以 MongoDB 内嵌数组的形式来存储,一次查询就能将订单所有的变更读取出来。
物联网场景,使用 MongoDB 存储所有接入的智能设备信息,以及设备汇报的日志信息,并对这些信息进行多维度的分析。
视频直播,使用 MongoDB 存储用户信息、点赞互动信息等。
这些应用场景中,数据操作方面的共同特点是:
(1)数据量大
(2)写入操作频繁(读写都很频繁)
(3)价值较低的数据,对事务性要求不高
对于这样的数据,我们更适合使用MongoDB来实现数据的存储

在架构选型上,除了上述的三个特点外,如果你还犹豫是否要选择它?可以考虑以下的一些问题:

应用不需要事务及复杂 join 支持
新应用,需求会变,数据模型无法确定,想快速迭代开发
应用需要2000-3000以上的读写QPS(更高也可以)
应用需要TB甚至 PB 级别数据存储
应用发展迅速,需要能快速水平扩展
应用要求存储的数据不丢失
应用需要99.999%高可用
应用需要大量的地理位置查询、文本查询
如果上述有1个符合,可以考虑 MongoDB,2个及以上的符合,选择 MongoDB 绝不会后悔。

2. MongoDB的安装

1、安装MongoDB,去官网下载并安装,随后将bin文件设置成环境变量path的变量。设置完毕,则设置自动启动MongoDB数据库的方式。

window教程:https://blog.csdn.net/m0_46217225/article/details/119109334?spm=1001.2014.3001.5501
centos7.6教程:https://www.cnblogs.com/mingerlcm/p/10656144.html

2、MongoDB常见的命令
– 开启服务器: net start MongoDB
– 关闭服务器: net stop MongoDB
– 打开客户端: mongo,在cmd窗口输入便可以连接上数据库

三、MongoDB常见命令

3、MongoDB的数据库分类
3.1、数据库(database):用来存储集合的,而且数据库也有分大小。
3.2、集合(collection):集合类似于数组,用于存放文档的
3.3、文档(document): 文档是MongoDB数据库中最小的单位,我们要操作的目标就是文档。

 

MongoDB关系: 数据库(database) > 集合(collection)> 文档(document)

值得注意的是: 在MongoDB中不需要自己创建数据库和集合,便可以直接创建文档,其实就是在创建文档的同时,会将数据库和集合创建了。

4、现在便是开始操作MongoDB数据库了
4.1、在使用之前可以安装一个编辑器(nosql manager for mongodb),下载免费版便可,可以不用在cmd界面操作。

mongodb  rebo3t安装路径:https://robomongo.org/download

4.2、软件下载完毕,便可以开始敲代码啦~
这是最基础的固定代码:

 

 

4.3、对mongodb数据库的CRUD操作

 

具体内容查看官方文档:    https://mongoosejs.com/

核心:    增: insert()    删: remove()    查: find()      改: update()

①增加

db.集合名.insert(document)    表示增加一个或多个文档
db.piyou.insert({name: "孙悟空", age: 28, address:"花果山"});
/*
    表示向 集合名为piyou插入了文档, 文档内容是对象。
    集合名是自己创建的,想用什么名称就用什么。
*/

db.piyou.insert([
    {name:"猪八戒", age: 38, address:"高老庄"}, 
    {name: "沙和尚", age: 45, address:"流沙河"}
]);
/*
    可以注意到: 传递的数据是数组,数组内部是对象,
        其实对象就相当于文档,这就是插入了两个文档。
*/
db.集合名.insertOne(document,[callback]) 
表示插入一个文档,回调函数callback是可选的
db.piyou.insertOne({name:"唐僧", age: 18, address: "女儿国"});
/*
    表示向集合名为piyou的 插入了一个文档。
*/
    db.集合名.insertMany(doucment, [callback]);
    表示插入多个文档
db.piyou.insertMany([
    {name:"白骨精", age:20,address:"白骨洞"}, 
    {name:"蜘蛛精", age: 24, address:"蜘蛛洞"}
]);
/*
    可以看到,用法是和insert是相差不多的。数组里面有对象,对象即是文档。
*/
②查找
要想查询上面插入的内容,便可调用查找的方法
db.集合名.find(condition);            查询一个或多个文档 condition是查询条件
db.集合名.findOne(condition);        查询一个文档
db.集合名.findMany(condition)        查询多个文档

下是查询语句和查询结果:

db.piyou.find({age:18});    
//这条语句表示的是查询age是18的文档。 值得注意的是,条件也是写在了对象里面,
//也是因为传入的值需要是json语句

执行结果:

 

 

 如果想要知道有多少个的时候,则可以使用

db.集合名.find(condition).count();    使用count()来计数
db.piyou.find().count();    // 执行结果是: 6, 因为在插入那边是一共插入了6条数据
总结:find()返回的是数组,数组内存放着文档,findOne()返回的就是一个文档,
findMany()返回的也是数组内存放着文档的形式。find()的返回值还可以调用count(),
用来获取符合条件的数量

③修改

db.集合名.update(condition,newObject);        修改一个或多个文档
db.集合名.uodateOne(condition,newObject);    修改一个文档
db.集合名.updateMany(condition, newObject);  修改多个文档
condition: 查询的条件        newObject: 需要修改的语句

首先需要注意的是,在使用update()时,需要一个新的玩意加入,叫做修改操作符,一般长成: 

    $set        表示需要设置指定的属性
    $unset        表示需要删除指定的属性
    $push        表示给数组添加一个新元素,因为文档内也会有数组,数组便会有数组元素
    $addToset     表示给数组添加一个新元素,和push的区别是,如果出现同名的数组元素,则不会再添加
    $gt            大于
    $gte        大于等于
    $lt            小于
    $lte        小于等于
    $or [{条件一,条件二}]        表示或的意思,符合条件一或者条件二    
    $inc        表示自增,用在在原来数据的基础上对数据加减,可用于加薪减薪的操作

所以不能像下面这条语句这样使用,这样使用的话,会使用{age:18}覆盖掉{name:“猪八戒”…}这整条语句。

db.piyou.update({name:"猪八戒"}, {age:39});

执行结果,可以看到猪八戒这整条数据不见了,那么应该怎么操作呢?这时候修改操作符就派上用场了

 

 

 

db.piyou.update({name:"孙悟空"}, {$set:{age:29}});
/*    
    表示根据条件{name:"孙悟空"}, 找到了孙悟空的这个文档, 使用了$set(修改指定属性)
    这个修改操作符,将age修改成了29
*/

执行结果:

 

db.piyou.update({name:"唐僧"}, {$unset:{address: 1}});
/*
    表示使用 $unset(删除指定属性), 将唐僧的address的属性值给删掉了
*/

执行结果,唐僧的address的值已经被删除了。

 

 

 

 ④删除

db.集合名.remove(condition)        删除符合条件的一个或多个文档
db.集合名.deleteOne(condition)    删除符合条件的一个文档
db.集合名.deleteMany(condition)    删除符合条件的多个文档
db.piyou.remove({age: 39});
/*
    表示删除了符合 age为39 这个条件的一个或多个文档。
    也就是删掉了刚刚那个猪八戒的那条语句
*/

执行结果

⑤小小练习一下

先看能不能读懂下面这条语句

 

db.persons.insert([
    {
        name:"大大白", 
        age: 28, 
        hobby:{
            music:['new Boy', '云烟成雨', '秋酿'],
            games:['王者荣耀', '和平精英', '光遇']
        }
    },
    {
        name: "小白",
        age: 21,
        hobby:{
            movies:['大话西游', '唐伯虎点秋香'],
            games:['王者荣耀', '旅行青蛙', '穿越火线']
        }
    }
]);
语句解释:
    向集合persons中插入一个数组,数组中有两个文档,文档内有一个hobby的文档,
    这个hobby文档被称作内嵌文档,然后hobby文档内有两个数组。

需求一:查询喜欢玩 王者荣耀 的人

分析:王者荣耀是存在于hobby中的games
db.persons.find({"hobby.games":"王者荣耀"});
/*
    这是需要注意的知识点:如果查询的是内嵌文档可以使用 . 的方式查询,
    不过需要使用引号,这是因为mongodb的文档是json的缘故吧。
*/

需求二: 插入20000条数据,有哪种是比较快捷的方式?
方式一:

for(let i = 1; i <= 20000; i++){
    db.nums.insert({num:i});
}
/*
    这是方式一,这种方式是需要调用insert语句20000次,效率会十分低下,那么可不可以,
    只调用一次insert语句呢?如果可以的话,应该怎么实现呢?
    可以将数据先存储起来,然后一次性加入。
*/

方式二:

const arr = [];
for(let i = 1; i <= 20000; i++){
    arr.push({num:i});
}
db.nums.insert(arr);
/*
    这是将数据全部存到了数组中,随后再将数组插入,只执行了一次insert语句。
    如果还有更好的想法都可以去尝试。还有就是自己给自己出题目,如果都能够实现的话,
    那么说明你学的差不多了。
*/

4.4、文档间的关系(三种)

①、一对一(one to one)

    -- 内嵌文档
    -- 夫妻关系
db.wife.insert([
    {
        name:"黄蓉",
        age: 26,
        handsband:{
            name:"郭靖",
            age: 38
        }
    },
    {
        name: "小兰",
        age: 16,
        handsband:{
            name: "新一",
            age: 16
        }
    }
]);
/*
    可以看到一个妻子是对应一个丈夫的,这是使用内嵌文档来实现的。
*/

②、一对多(one to many)

-- 父母 - 孩子
-- 文章 - 评论
-- 内嵌文档也可以实现,就是一个文档内嵌多个文档,不过比较繁琐
-- 用户一个文档,订单一个文档,在使用的时候再一一对应
// 第一句是创建了两个用户, 分别是花木兰和诸葛亮
db.users.insert([{name:"花木兰"}, {name:"诸葛亮"}]);
// 这一句是查询用户内容, 这是需要知道用户的_id,从而在第三句加入
db.users.find();
// 这是生成了一个订单,并且user_id存储的是花木兰的id
db.orders.insert({list_name:["配马", "鞍鞯"], user_id: ObjectId("61374eac77393663e2de9bd3")})
// 再生成了一个订单,并且user_id存储的是花木兰的id
db.orders.insert({list_name:["蜜糖", "男装"], user_id: ObjectId("61374eac77393663e2de9bd3")})
// 在users里查找到 花木兰 这个用户的id,并将其存储起来
let userId = db.users.findOne({name:"花木兰"})._id;
// 使用id来查找
db.orders.find({user_id: userId});

③、多对多(many to many)

    -- 学生 - 老师
    -- 商品 - 分类
    -- 可以使用内嵌文档的形式完成
db.teas.insert([{name:"诸葛亮"}, {name:"龟仙人"}, {name:"唐僧"}]);
db.teas.find();
db.stus.insert([
    {
        name: "孙悟空",
        // 插入的是 龟仙人和唐僧的id
        teas_id:[ObjectId("6137552877393663e2de9bdb"), ObjectId("6137552877393663e2de9bdc")]
    },
    {
        name: "刘禅",
        // 插入的是 诸葛亮和龟仙人的id
        teas_id:[ObjectId("6137552877393663e2de9bda"),ObjectId("6137552877393663e2de9bdb")]
    }
        
]);

db.stus.find();

4.5、投影(就是限制条件,显示效果)
首先先创建一个集合:

db.staff.insert([
    {name:"小白", age: 18, salary: 3000},
    {name:"大白", age: 19, salary: 3500},
    {name:"熊大", age: 23, salary: 3200},
    {name:"熊二", age: 22, salary: 3100},
    {name:"光头强", age: 27, salary: 3400},
    {name:"小鲤鱼", age: 17, salary: 1500},
    {name:"奥特曼", age: 39, salary: 4500}
]);
/*
    这样创建的是一个列表集合。
*/

需求一:按照薪资排列,可以使用sort()方法

    sort(condition)    
    sort({salary:  1})表示按照薪资升序排列
    sort({salary: -1})表示按照薪资降序排列
db.staff.find().sort({salary: 1});

排列结果:

 

db.staff.find().sort({salary: 1, age: -1});
/*
    这句的含义是按照薪资升序排序,如果出现了相同的薪资的人,
    则按照年龄降序排序。
*/
limit() 限制 和skip() 跳过, 这两个方法经常可以一起使用,从而用来翻页。
而且这两个方法是可以不分先后的。
db.staff.find().limit(2);
/*
    表示只会显示前两个数据
*/

执行结果:

 

 

 

在查询时可以在find()中的第二个参数的位置传入 投影,
{name: 1, _id: 0}: 表示显示name属性,不显示_id属性。
db.staff.find({}, {name: 1, _id: 0, salary: 1});
/*
    这句的含义是只显示 name和salary属性,不显示 _id属性
*/

执行结果:

 

4. MongoDB索引

MongoDB索引对应的官网文档介绍:添加链接描述

索引支持在MongoDB中高效执行查询。如果没有索引,MongoDB必须执行集合扫描,即扫描集合中的每个文档,以选择与查询语句匹配的文档。如果查询存在适当的索引,MongoDB可以使用该索引限制它必须检查的文档数量。

索引是一种特殊的数据结构[1],它以易于遍历的形式存储集合数据集的一小部分。索引存储特定字段或字段集的值,按字段值排序。索引项的顺序支持有效的相等匹配和基于范围的查询操作。此外,MongoDB可以使用索引中的排序返回排序结果。

4.1 概述

索引支持在MongoDB中高效地执行查询。如果没有索引,MongoDB必须执行全集合扫描,即扫描集合中的每个文档,以选择与查询语句

匹配的文档。这种扫描全集合的查询效率是非常低的,特别在处理大量的数据时,查询可以要花费几十秒甚至几分钟,这对网站的性能是非
常致命的。
如果查询存在适当的索引,MongoDB可以使用该索引限制必须检查的文档数。
索引是特殊的数据结构,它以易于遍历的形式存储集合数据集的一小部分。索引存储特定字段或一组字段的值,按字段值排序。索引项的排
序支持有效的相等匹配和基于范围的查询操作。此外,MongoDB还可以使用索引中的排序返回排序结果。

官网文档:https://docs.mongodb.com/manual/indexes/
了解:
MongoDB索引使用B树数据结构(确切的说是B-Tree,MySQL是B+Tree)

4.2 MongoDB索引的数据结构

4.3 索引的管理操作

4.3.1 索引的查看

返回一个集合中的所有索引的数组。语法格式:

db.collection.getIndexes()

该语法命令运行要求是MongoDB 3.0+

【示例】
查看comment集合中所有的索引情况

结果中显示的是默认 _id 索引。
默认_id索引:

 

 

 

MongoDB在创建集合的过程中,在 _id 字段上创建一个唯一的索引,默认名字为 id ,该索引可防止客户端插入两个具有相同值的文
档,您不能在_id字段上删除此索引。

注意:该索引是唯一索引,因此值不能重复,即 _id 值不能重复的。在分片集群中,通常使用 _id 作为片键。

4.3.2 索引的创建

在集合上创建索引,语法格式:

db.collection.createIndex(keys, options)

参数说明:

 

 

 options(更多选项)列表说明:

 

 

 注意在 3.0.0 版本前创建索引方法为 db.collection.ensureIndex() ,之后的版本使用了 db.collection.createIndex() 方法,ensureIndex() 还能用,但只是 createIndex() 的别名。

4.3.3 索引的移除

索引的移除,可以移除指定的索引,或移除所有索引。
1、指定索引的移除,语法格式如下:

db.collection.dropIndex(index)

 

 

 2、所有索引的移除,语法格式:

db.collection.dropIndexes()

_id 的字段的索引是无法删除的,只能删除非 _id 字段的索引。

4.3.4 执行计划

分析查询性能(Analyze Query Performance)通常使用执行计划(解释计划、Explain Plan)来查看查询的情况,如查询耗费的时间、是否基于索引查询等。那么,通常,我们想知道,建立的索引是否有效,效果如何,都需要通过执行计划查看。语法格式:

db.collection.find(query,options).explain(options)

【示例】
查看根据userid查询数据的情况:

 

 

 关键点看: “stage” : “COLLSCAN”, 表示全集合扫描, “stage” : “IXSCAN” ,基于索引的扫描。

 

拓展:
涵盖的查询,又可称为索引覆盖,当查询条件和查询的投影仅包含索引字段时,MongoDB直接从索引返回结果,而不扫描任何文档或将文档带入内存。 这些覆盖的查询可以非常有效。

 

更多:https://docs.mongodb.com/manual/core/query-optimization/#read-operations-covered-query

 

 

4.4 索引的类型

4.4.1 单字段索引(单键索引)

MongoDB支持在文档的单个字段上创建用户定义的升序/降序索引,称为单字段索引(Single Field Index)。

对于单个字段索引和排序操作,索引键的排序顺序(即升序或降序)并不重要,因为MongoDB可以在任何方向上遍历索引。因为document的存储是bson格式的,我们也可以给内置对象的字段添加索引,或者将整个内置对象作为一个索引。

 

【示例】
给comment集合userid字段添加升序索引

db.comment.createIndex({userid:1})

 

 

 

如果想设置倒序索引的话使用 db.comment.createIndex({userid:-1}) 即可。我们通过explain()方法查看查询计划,如下图,看到查询userid=1003的document时使用了索引,如果没有使用索引的话stage=COLLSCAN。使用了索引stage=IXSCAN。

4.4.2 复合索引

 

MongoDB还支持多个字段的用户定义索引,即复合索引(Compound Index)。
复合索引中列出的字段顺序具有重要意义。例如,如果复合索引由 { userid: 1, likenum: -1 } 组成,则索引首先按userid正序排序,然后在每个userid的值内,再在按likenum倒序排序。

 

 

 与MySQL的复合索引一样需要遵循最左前缀法则,如果查询条件只用到了likenum,不会使用到刚才创建的复合索引,而是全表扫描。

 

 

 

4.4.3 多键索引

多键索引(mutiKey Indexes)是建在数组上的索引,在MongoDB的document中,有些字段的值为数组,多键索引就是为了提高查询这些数组的效率。看一个栗子:准备测试数据,classes集合中添加两个班级,每个班级都有一个students数组,如下:

db.classes.insertMany([
     {
         "classname":"class1",
         "students":[{name:'jack',age:20},
                    {name:'tom',age:22},
                    {name:'lilei',age:25}]
      },
      {
         "classname":"class2",
         "students":[{name:'lucy',age:20},
                    {name:'jim',age:23},
                    {name:'jarry',age:26}]
      }]
  )

使用db.classes.createIndex({'students.name':1})添加索引,查看执行计划,isMultikey表示使用的是多键索引。

 

 

 

4.4.4 哈希索引

为了支持基于散列的分片,MongoDB提供了散列索引类型,它对字段值的散列进行索引。这些索引在其范围内的值分布更加随机,但只支持相等匹配,不支持基于范围的查询。

下面创建哈希索引,并查看对应的执行计划。

db.comment.createIndex( { nickname: "hashed" } )

 

 

 哈希索引(hashed Indexes)就是将field的值进行hash计算后作为索引,其强大之处在于实现O(1)查找,当然用哈希索引最主要的功能也就是实现定值查找,对于经常需要排序或查询范围查询的集合不要使用哈希索引。

4.4.5 其他索引

其他索引还包括有地理空间索引(Geospatial Index)、文本索引(Text Indexes)等)。

地理空间索引(Geospatial Index)
为了支持对地理空间坐标数据的有效查询,MongoDB提供了两种特殊的索引:返回结果时使用平面几何的二维索引和返回结果时使用球面
几何的二维球面索引。
文本索引(Text Indexes)
MongoDB提供了一种文本索引类型,支持在集合中搜索字符串内容。这些文本索引不存储特定于语言的停止词(例如“the”、“a”、“or”),而将集合中的词作为词干,只存储根词。

4.5 索引的常用属性

前面提到的在集合上创建索引,语法格式:

db.collection.createIndex(keys, options)

options定义不同的属性,该索引有不同的功能,下面介绍创建索引时常见的几个属性。

4.5.1 唯一索引

唯一索引(unique indexes)用于为collection集合添加唯一约束,即强制要求collection中的唯一索引字段没有重复值。

 

 无法插入建立了唯一索引的字段上存在重复的文档

 

 

4.5.2 局部索引

局部索引(Partial Indexes)顾名思义,只对collection的一部分添加索引。创建索引的时候,根据过滤条件判断是否对document添加索引,对于没有添加索引的文档查找时采用的全表扫描,对添加了索引的文档查找时使用索引。

 db.comment.createIndex({articleid:1},{partialFilterExpression:{articleid:'100001'}})

对于article不等于100001的文档,查询没有用到索引。

 

 对于article等于100001的文档,查询使用到索引。

 

 

4.5.3 稀疏索引

稀疏索引(sparse indexes),又称间隙索引,在有索引字段的document上添加索引,如在address字段上添加稀疏索引时,只有document有address字段时才会添加索引。而普通索引则是为所有的document添加索引,使用普通索引时如果document没有索引字段的话,设置索引字段的值为null。

稀疏索引的创建方式如下,当document包含address字段时才会创建索引:

db.comment.createIndex({nickname:1},{sparse:true})

 

 ==如果一个间隙索引会导致查询或者排序操作得到一个不完整结果集的时候,MongoDB将不会使用这个索引,而是选择集合扫面。==稀疏索引无法使用的情况示例:

 

 

因为在唯一索引中,唯一索引会把null当做值,也就是说为null的通常只能有一个。后面的null将无法插入。而给字段建立一个既包含稀疏又包含唯一的索引因为忽略null值,可以避免唯一索引的校验。

2dsphere (version 2), 2d, geoHaystack, 文本索引等在索引字段缺省的情况下总是稀疏索引。

4.5.4 TTL索引

TTL索引(TTL indexes)是一种特殊的单键索引,用于设置文档的过期时间,MongoDB会在文档过期后将其删除,TTL非常容易实现类似缓存过期策略的功能。

db.logs.insertMany([
       {_id:1,createtime:new Date(),msg:"log1"},
       {_id:2,createtime:new Date(),msg:"log2"},
       {_id:3,createtime:new Date(),msg:"log3"},
       {_id:4,createtime:new Date(),msg:"log4"}
       ]);
> db.logs.insertMany([
...        {_id:1,createtime:new Date(),msg:"log1"},
...        {_id:2,createtime:new Date(),msg:"log2"},
...        {_id:3,createtime:new Date(),msg:"log3"},
...        {_id:4,createtime:new Date(),msg:"log4"}
...        ]);
{ "acknowledged" : true, "insertedIds" : [ 1, 2, 3, 4 ] }
//给logs集合中_id等于1的文档添加TTL索引,过期时间为30s
> db.logs.createIndex({createtime:1},{expireAfterSeconds:30,partialFilterExpression:{_id:1}})
{
    "numIndexesBefore" : 1,
    "numIndexesAfter" : 2,
    "createdCollectionAutomatically" : false,
    "ok" : 1
}

注意:TTL索引只能设置在date类型字段(或者包含date类型的数组)上,过期时间为字段值+exprireAfterSeconds;document过期时不一定就会被立即删除,因为MongoDB执行删除任务的时间间隔是60s;capped Collection(固定索引)不能设置TTL索引,因为MongoDB不能主动删除capped Collection中的document。

MongoDB 聚合

MongoDB 中聚合(aggregate)主要用于处理数据(诸如统计平均值,求和等),并返回计算后的数据结果。

有点类似 SQL 语句中的 count(*)。

aggregate() 方法

MongoDB中聚合的方法使用aggregate()。

语法

aggregate() 方法的基本语法格式如下所示:

db.集合名.aggregate(AGGREGATE_OPERATION)

实例

集合中的数据如下:

{
   _id: ObjectId(7df78ad8902c)
   title: 'MongoDB Overview', 
   description: 'MongoDB is no sql database',
   by_user: 'runoob.com',
   url: 'http://www.runoob.com',
   tags: ['mongodb', 'database', 'NoSQL'],
   likes: 100
},
{
   _id: ObjectId(7df78ad8902d)
   title: 'NoSQL Overview', 
   description: 'No sql database is very fast',
   by_user: 'runoob.com',
   url: 'http://www.runoob.com',
   tags: ['mongodb', 'database', 'NoSQL'],
   likes: 10
},
{
   _id: ObjectId(7df78ad8902e)
   title: 'Neo4j Overview', 
   description: 'Neo4j is no sql database',
   by_user: 'Neo4j',
   url: 'http://www.neo4j.com',
   tags: ['neo4j', 'database', 'NoSQL'],
   likes: 750
},
b.mycol.insert([{
   "title":"MongoDB Overview", 
   "description": 'MongoDB is no sql database',
   "by_user": 'runoob.com',
   "url": 'http://www.runoob.com',
   "tags": ['mongodb', 'database', 'NoSQL'],
   "likes": 100
},{

   "title": 'NoSQL Overview', 
   "description": 'No sql database is very fast',
   "by_user": 'runoob.com',
   "url": 'http://www.runoob.com',
   "tags": ['mongodb', 'database', 'NoSQL'],
   "likes": 10
},{
   "title": 'Neo4j Overview', 
   "description": 'Neo4j is no sql database',
   "by_user": 'Neo4j',
   "url": 'http://www.neo4j.com',
   "tags": ['neo4j', 'database', 'NoSQL'],
   "likes": 750
}])

现在我们通过以上集合计算每个作者所写的文章数,使用aggregate()计算结果如下:

db.mycol.aggregate([{$group : {_id : "$by_user", num_tutorial : {$sum : 1}}}])

 

以上实例类似sql语句:

 

select by_user, count(*) from mycol group by by_user

在上面的例子中,我们通过字段 by_user 字段对数据进行分组,并计算 by_user 字段相同值的总和。

下表展示了一些聚合的表达式:

表达式描述实例
$sum 计算总和。 db.mycol.aggregate([{$group : {_id : "$by_user", num_tutorial : {$sum : "$likes"}}}])
$avg 计算平均值 db.mycol.aggregate([{$group : {_id : "$by_user", num_tutorial : {$avg : "$likes"}}}])
$min 获取集合中所有文档对应值得最小值。 db.mycol.aggregate([{$group : {_id : "$by_user", num_tutorial : {$min : "$likes"}}}])
$max 获取集合中所有文档对应值得最大值。 db.mycol.aggregate([{$group : {_id : "$by_user", num_tutorial : {$max : "$likes"}}}])
$push 将值加入一个数组中,不会判断是否有重复的值。 db.mycol.aggregate([{$group : {_id : "$by_user", url : {$push: "$url"}}}])
$addToSet 将值加入一个数组中,会判断是否有重复的值,若相同的值在数组中已经存在了,则不加入。 db.mycol.aggregate([{$group : {_id : "$by_user", url : {$addToSet : "$url"}}}])
$first 根据资源文档的排序获取第一个文档数据。 db.mycol.aggregate([{$group : {_id : "$by_user", first_url : {$first : "$url"}}}])
$last 根据资源文档的排序获取最后一个文档数据 db.mycol.aggregate([{$group : {_id : "$by_user", last_url : {$last : "$url"}}}])

计算总和

 

 

 计算平均值

 

后面不在截图大家一一试一下。

管道的概念

 

管道在Unix和Linux中一般用于将当前命令的输出结果作为下一个命令的参数。

MongoDB的聚合管道将MongoDB文档在一个管道处理完毕后将结果传递给下一个管道处理。管道操作是可以重复的。

表达式:处理输入文档并输出。表达式是无状态的,只能用于计算当前聚合管道的文档,不能处理其它的文档。

这里我们介绍一下聚合框架中常用的几个操作:

  • $project:修改输入文档的结构。可以用来重命名、增加或删除域,也可以用于创建计算结果以及嵌套文档。
  • $match:用于过滤数据,只输出符合条件的文档。$match使用MongoDB的标准查询操作。
  • $limit:用来限制MongoDB聚合管道返回的文档数。
  • $skip:在聚合管道中跳过指定数量的文档,并返回余下的文档。
  • $unwind:将文档中的某一个数组类型字段拆分成多条,每条包含数组中的一个值。
  • $group:将集合中的文档分组,可用于统计结果。
  • $sort:将输入文档排序后输出。
  • $geoNear:输出接近某一地理位置的有序文档。

管道操作符实例

db.mycol.aggregate( [
                        { $match : { score : { $gt : 70, $lte : 90 } } },
                        { $group: { _id: null, count: { $sum: 1 } } }
                       ] );

 

1、$project实例

db.mycol.aggregate(
    { $project : {
        title : 1 ,
        by_user : 1 ,
    }}
 );

这样的话结果中就只还有_id,tilte和author三个字段了,默认情况下_id字段是被包含的,如果要想不包含_id话可以这样:

db.mycol.aggregate(
    { $project : {
        _id : 0 ,
        title : 1 ,
        by_user : 1
    }});

2.$match实例

$match用于获取分数大于70小于或等于90记录,然后将符合条件的记录送到下一阶段$group管道操作符进行处理。

3.$skip实例

db.mycol.aggregate(
    { $skip : 1 }
    );

组合使用,先查询like大于10小于2000的,然后对like进行降序排序,跳过第一个,再取第一个,最后对数据进行过滤

db.mycol.aggregate([{ $match : { likes : { $gt : 10, $lte : 2000 } } },{$sort:{likes:1}},
    { $skip : 1 }, { $limit : 1 },{ $project : {
        _id : 0 ,
        title : 1 ,
        by_user : 1
    }}
    ]);

 

5 文章评论

5.1 需求分析

某头条的文章评论业务如下:
文章示例参考:早晨空腹喝水,是对还是错?
需要实现以下功能:
1)基本增删改查API
2)根据文章id查询评论
3)评论点赞

5.2 表结构分析

数据库:articledb
 
 
专栏文章评论
comment
   
字段名称
字段含义
字段类型
备注
_id
ID
ObjectId或String 
Mongo的主键的字段
articleid
文章ID 
String
 
content
评论内容 
String
 
userid
评论人ID 
String
 
nickname
评论人昵称 
String
 
createdatetime
评论的日期时间 
Date
 
likenum
点赞数 
Int32
 
replynum
回复数 
 
Int32
 
state
状态 
String 
0:不可见;1:可见;
parentid
上级ID 
String
如果为0表示文章的顶级评论

5.3 技术选型

5.3.1 mongodb-driver(了解)
mongodb-driver是mongo官方推出的java连接mongoDB的驱动包,相当于JDBC驱动。我们通过一个入门的案例来了解mongodb-driver
的基本使用。
官方驱动说明和下载:http://mongodb.github.io/mongo-java-driver/
官方驱动示例文档:http://mongodb.github.io/mongo-java-driver/3.8/driver/getting-started/quick-start/
5.3.2 SpringDataMongoDB
SpringData家族成员之一,用于操作MongoDB的持久层框架,封装了底层的mongodb-driver。
官网主页:
https://projects.spring.io/spring-data-mongodb/
我们十次方项目的吐槽微服务就采用SpringDataMongoDB框架。
 

5.4 文章微服务模块搭建

(1)搭建项目工程article,pom.xml引入依赖:
<?xml version="1.0" encoding="UTF-8"?> <project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd"> <modelVersion>4.0.0</modelVersion> <parent> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-parent</artifactId> <version>2.1.6.RELEASE</version> <relativePath/> <!-- lookup parent from repository --> </parent> <groupId>cn.itcast</groupId> <artifactId>article</artifactId> <version>1.0-SNAPSHOT</version> <dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-mongodb</artifactId>
</dependency>
</dependencies>
</project>
(2)创建application.yml
spring: #数据源配置 data: mongodb: # 主机地址 host: 192.168.40.141 # 数据库 database: articledb # 默认端口是27017 port: 27017 #也可以使用uri连接 #uri: mongodb://192.168.40.134:27017/articledb
(3)创建启动类
cn.itcast.article.ArticleApplication
package cn.itcast.article; import org.springframework.boot.SpringApplication; import org.springframework.boot.autoconfigure.SpringBootApplication; @SpringBootApplication public class ArticleApplication { public static void main(String[] args) { SpringApplication.run(ArticleApplication.class, args); } }
4)启动项目,看是否能正常启动,控制台没有错误。

5.5 文章评论实体类的编写

创建实体类 创建包cn.itcast.article,包下建包po用于存放实体类,创建实体类
cn.itcast.article.po.Comment
package cn.itcast.article.po; import org.springframework.data.annotation.Id; import org.springframework.data.mongodb.core.index.Indexed; import org.springframework.data.mongodb.core.mapping.Document; import org.springframework.data.mongodb.core.mapping.Field; import java.io.Serializable; import java.time.LocalDateTime; import java.util.Date; /*** 文章评论实体类 */ //把一个java类声明为mongodb的文档,可以通过collection参数指定这个类对应的文档。 //@Document(collection="mongodb 对应 collection 名") // 若未加 @Document ,该 bean save 到 mongo 的 comment collection // 若添加 @Document ,则 save 到 comment collection @Document(collection="comment")//可以省略,如果省略,则默认使用类名小写映射集合 //复合索引 // @CompoundIndex( def = "{'userid': 1, 'nickname': -1}") public class Comment implements Serializable { //主键标识,该属性的值会自动对应mongodb的主键字段"_id",如果该属性名就叫“id”,则该注解可以省略,否则必须写 @Id private String id;//主键 //该属性对应mongodb的字段的名字,如果一致,则无需该注解 @Field("content")
private String content;//吐槽内容

private Date publishtime;//发布日期

//添加了一个单字段的索引

@Indexed

private String userid;//发布人ID

private String nickname;//昵称

private LocalDateTime createdatetime;//评论的日期时间

private Integer likenum;//点赞数

private Integer replynum;//回复数

private String state;//状态

private String parentid;//上级ID

private String articleid;

//getter and setter.....

public String getId() {

return id;

}

public void setId(String id) {

this.id = id;

}

public String getContent() {

return content;

}

public void setContent(String content) {

this.content = content;

}

public Date getPublishtime() {

return publishtime;

}

public void setPublishtime(Date publishtime) {

this.publishtime = publishtime;

}

public String getUserid() {

return userid;

}

public void setUserid(String userid) {

this.userid = userid;

}

public String getNickname() {

return nickname;

}

public void setNickname(String nickname) {

this.nickname = nickname;

}

public LocalDateTime getCreatedatetime() {

return createdatetime;

}

public void setCreatedatetime(LocalDateTime createdatetime) {

this.createdatetime = createdatetime;

}

public Integer getLikenum() {

return likenum;

}

public void setLikenum(Integer likenum) {

this.likenum = likenum;

}

public Integer getReplynum() {

return replynum;

}

public void setReplynum(Integer replynum) {

this.replynum = replynum;

}

public String getState() {

return state;

}

public void setState(String state) {

this.state = state;

}

public String getParentid() {
return parentid;
}
public void setParentid(String parentid) {
this.parentid = parentid;
}
public String getArticleid() {
return articleid;
}
public void setArticleid(String articleid) {
this.articleid = articleid;
}
@Override
public String toString() {
return "Comment{" +
"id='" + id + '\'' +
", content='" + content + '\'' +
", publishtime=" + publishtime +
", userid='" + userid + '\'' +
", nickname='" + nickname + '\'' +
", createdatetime=" + createdatetime +
", likenum=" + likenum +
", replynum=" + replynum +
", state='" + state + '\'' +
", parentid='" + parentid + '\'' +
", articleid='" + articleid + '\'' +
'}';
}
}
说明:
索引可以大大提升查询效率,一般在查询字段上添加索引,索引的添加可以通过Mongo的命令来添加,也可以在Java的实体类中通过注解添
加。
1)单字段索引注解@Indexed
org.springframework.data.mongodb.core.index.Indexed.class
声明该字段需要索引,建索引可以大大的提高查询效率。
Mongo命令参考:
db.comment.createIndex({"userid":1})
2)复合索引注解@CompoundIndex
org.springframework.data.mongodb.core.index.CompoundIndex.class
复合索引的声明,建复合索引可以有效地提高多字段的查询效率。
Mongo命令参考:
db.comment.createIndex({"userid":1,"nickname":-1})

5.6 文章评论的基本增删改查

(1)创建数据访问接口 cn.itcast.article包下创建dao包,包下创建接口
cn.itcast.article.dao.CommentRepository
package cn.itcast.article.dao; import cn.itcast.article.po.Comment; import org.springframework.data.domain.Page; import org.springframework.data.domain.Pageable; import org.springframework.data.mongodb.repository.MongoRepository; //评论的持久层接口 public interface CommentRepository extends MongoRepository<Comment,String> { }
(2)创建业务逻辑类 cn.itcast.article包下创建service包,包下创建类
cn.itcast.article.service.CommentService
package cn.itcast.article.service; import cn.itcast.article.dao.CommentRepository;
import cn.itcast.article.po.Comment; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Service; import java.util.List; //评论的业务层 @Service public class CommentService { //注入dao @Autowired private CommentRepository commentRepository; /*** 保存一个评论 * @param comment */ public void saveComment(Comment comment){ //如果需要自定义主键,可以在这里指定主键;如果不指定主键,MongoDB会自动生成主键 //设置一些默认初始值。。。 //调用dao commentRepository.save(comment); }/*** 更新评论 * @param comment */ public void updateComment(Comment comment){ //调用dao commentRepository.save(comment); }/*** 根据id删除评论 * @param id */ public void deleteCommentById(String id){ //调用dao commentRepository.deleteById(id); }/*** 查询所有评论 * @return */ public List<Comment> findCommentList(){ //调用dao return commentRepository.findAll(); }/*** 根据id查询评论 * @param id * @return */ public Comment findCommentById(String id){ //调用dao return commentRepository.findById(id).get(); } }
(3)新建Junit测试类,测试保存和查询所有:
cn.itcast.article.service.CommentServiceTest
package cn.itcast.article.service; import cn.itcast.article.ArticleApplication; import cn.itcast.article.po.Comment; import org.junit.Test; import org.junit.runner.RunWith; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.boot.test.context.SpringBootTest; import org.springframework.data.domain.Page; import org.springframework.test.context.junit4.SpringRunner; import java.time.LocalDateTime; import java.util.List; //测试评论的业务层
//SpringBoot的Junit集成测试

@RunWith(SpringRunner.class)

//SpringBoot的测试环境初始化,参数:启动类

@SpringBootTest(classes = ArticleApplication.class)

public class CommentServiceTest {

//注入Service

@Autowired

private CommentService commentService;

/**

* 保存一个评论

*/

@Test

public void testSaveComment(){

Comment comment=new Comment();

comment.setArticleid("100000");

comment.setContent("测试添加的数据");

comment.setCreatedatetime(LocalDateTime.now());

comment.setUserid("1003");

comment.setNickname("凯撒大帝");

comment.setState("1");

comment.setLikenum(0);

comment.setReplynum(0);

commentService.saveComment(comment);

}

/**

* 查询所有数据

*/

@Test

public void testFindAll(){

List<Comment> list = commentService.findCommentList();

System.out.println(list);

}

/**

* 测试根据id查询

*/

@Test

public void testFindCommentById(){

Comment comment = commentService.findCommentById("5d6a27b81b8d374798cf0b41");

System.out.println(comment);

}

}
 
 
 

 

 

 

5.7 根据上级ID查询文章评论的分页列表

(1)CommentRepository新增方法定义
 
//根据父id,查询子评论的分页列表 Page<Comment> findByParentid(String parentid, Pageable pageable);
(2)CommentService新增方法
/*** 根据父id查询分页列表 * @param parentid * @param page * @param size * @return */ public Page<Comment> findCommentListPageByParentid(String parentid,int page ,int size){ return commentRepository.findByParentid(parentid, PageRequest.of(page-1,size)); }
(3)junit测试用例:
cn.itcast.article.service.CommentServiceTest
/** * 测试根据父id查询子评论的分页列表 */ @Test public void testFindCommentListPageByParentid(){ Page<Comment> pageResponse = commentService.findCommentListPageByParentid("3", 1, 2); System.out.println("----总记录数:"+pageResponse.getTotalElements()); System.out.println("----当前页数据:"+pageResponse.getContent()); }
(4)测试
使用compass快速插入一条测试数据,数据的内容是对3号评论内容进行评论。
 

 

 

执行测试,结果:
----总记录数:1 ----当前页数据:[Comment{id='33', content='你年轻,火力大', publishtime=null, userid='1003', nickname='凯撒大帝', createdatetime=null, likenum=null, replynum=null, state='null', parentid='3', articleid='100001'}]

5.8 MongoTemplate实现评论点赞

我们看一下以下点赞的临时示例代码: CommentService 新增updateThumbup方法
/*** 点赞-效率低 * @param id */ public void updateCommentThumbupToIncrementingOld(String id){ Comment comment = CommentRepository.findById(id).get(); comment.setLikenum(comment.getLikenum()+1); CommentRepository.save(comment); }
以上方法虽然实现起来比较简单,但是执行效率并不高,因为我只需要将点赞数加1就可以了,没必要查询出所有字段修改后再更新所有字
段。(蝴蝶效应)
我们可以使用MongoTemplate类来实现对某列的操作。 (1)修改CommentService
//注入MongoTemplate @Autowired private MongoTemplate mongoTemplate;
/*** 点赞数+1 * @param id */ public void updateCommentLikenum(String id){ //查询对象 Query query=Query.query(Criteria.where("_id").is(id)); //更新对象 Update update=new Update(); //局部更新,相当于$set // update.set(key,value) //递增$inc // update.inc("likenum",1); update.inc("likenum"); //参数1:查询对象 //参数2:更新对象 //参数3:集合的名字或实体类的类型Comment.class mongoTemplate.updateFirst(query,update,"comment"); }
(2)测试用例:
cn.itcast.article.service.CommentServiceTest
/*** 点赞数+1 */ @Test public void testUpdateCommentLikenum(){ //对3号文档的点赞数+1 commentService.updateCommentLikenum("3"); }
执行测试用例后,发现点赞数+1了:

 

 

posted @ 2022-01-13 10:33  小陈子博客  阅读(115)  评论(0编辑  收藏  举报