MongoDB


MongoDB 官网:https://www.mongodb.com/zh

windows 下安装 mongodb

下载msi安装程序

一步一步安装即可(具体看视频)

MacOS 下安装 mongodb

安装流程:https://docs.mongodb.com/manual/tutorial/install-mongodb-on-os-x/

首先安装homebrew
brew tap mongodb/brew
brew install mongodb-community@5.0

注意事项:

在安装过程中如果出现权限不足的问题,输入命令:

sudo chown -R $(whoami) /usr/local/include /usr/local/lib /usr/local/lib/pkgconfig

添加开启自启动服务时需要使用sudo
sudo brew services start mongodb-community@5.0
mongo
show dbs;

安装 robo 3T

类似于mysqlnavicat

下载地址:https://robomongo.org/

基础概念

  • db:和mysql的概念一致
  • collection:集合,类似于mysql中的表
  • document:每个集合中的文档,类似于mysql中的记录
    • Primary Key:和mysql中的主键含义一致,每个document都有一个主键
    • field:文档中的字段

mongodb属于nosql中的文档型数据库,每个文档相当于是一个对象,它没有列的概念,也没有表关系

由于它是一个nosql数据库:

  • sql语句
  • 使用极其简单,学习成本非常低
  • 由于没有集合之间的关联,难以表达复杂的数据关系
  • 存取速度极快

由于它是一个文档型数据库:

  • 数据内容非常丰富和灵活
  • 对数据结构难以进行有效的限制

基本操作

通过mongo命令,即可进入mongodbshell交互环境,在shell环境中,很多时候都可以使用简单的js语句即可完成对mongodb的控制

下面是mongo的常用命令:

  1. 查看所有数据库:
show dbs;
  1. 显示当前使用的数据库:
db;//test。可以使用不存在的数据库
  1. 查看当前数据库状态:
db.stats()
  1. 查看数据库中所有的集合:
show collections;
  1. 切换数据库:
use 数据库名;
  1. 向集合中添加文档:
db.collection.insertOne({ 文档内容 });
db.collection.insertMany([多个文档]);

新的文档如果没有指定字段_id,则会自动添加一个字段_id作为主键

自动的主键是一个ObjectId对象,该对象是通过调用函数ObjectId()创建的

它的原理是根据时间戳+机器码+进程Id+自增量生成的一个十六进制的唯一字符串

使用ObjectId函数还可以把某个字符串还原成一个ObjectId对象,例如ObjectId("xxxxx")

  1. 查询文档:
db.collection.find(查询对象);
  1. 修改文档:

https://docs.mongodb.com/manual/tutorial/update-documents/

db.collection.updateOne(<filter>, <update>)// 条件 修改的值
db.collection.updateMany(<filter>, <update>)
db.collection.replaceOne(<filter>, <update>) // 完全覆盖
  1. 删除文档:
db.collection.deleteMany(查询对象)
db.collection.deleteOne(查询对象)

schema 和 model

mongodb 的驱动就叫做mongodb

创建连接、模型定义,CRUD

mongoose 官网:https://mongoosejs.com/

mongoose 民间中文网:http://www.mongoosejs.net/

schema: 结构,描述某种数据中有哪些字段、每个字段是什么类型、每个字段的约束

模型:对应数据库中集合的文档

模型:

  1. 用户:
{
  loginId:"账号",
  loginPwd:"密码",
  name:"姓名",
  loves: ["爱好"],
  address: {
    province: "省份",
    city: "城市"
  }
}
  1. 用户操作:
{
  operation: "登录",
  time: 日期,
  userid: 用户的id,
  extraInfo: 任意对象, // 操作额外的信息
  address: { // 操作的地址
    province: "省份",
    city: "城市"
  }
}

新增文档

mongodb 原生操作

// 新增单个数据,doc是一个文档对象
db.<collection>.insertOne(doc);

// 新增多个数据,docs是一个文档数组
db.<collection>.insertMany(docs);

// 新增单个或多个数据,返回新增的行数,doc即可以是一个文档对象,也可以是一个文档数组
db.<collection>.insert(doc);

db.users.insert({loginId:"aaa"})

mognoose 中的所有验证规则在原生操作中无效

mongoose 操作

方式 1:创建模型对象,然后保存

var obj = new <Model>(doc);
var result = await obj.save(); // 保存文档到数据库,会触发验证,也可以使用回调模式

方式 2:直接使用函数创建对象

// 创建一个或多个文档
// 也可以使用回调模式
// 若传入单个对象,返回单个对象
// 若传入多个对象,返回一个数组
var result = await <Model>.create(...doc);

创建操作的细节

  • mongoose会为每个对象(包括子对象)添加唯一键_id,这是一种极好的的做法,特别是在对象数组中,可以有效的维护数据的唯一标识

    • 如果希望禁用这种做法,只需要在相应的Schema中配置_id: false
  • mongoose在创建文档时,会自动生成一个字段__v,该字段用于方式并发冲突(后续中说明)

    • 如果希望禁用这种做法,只需要在Schema的第二个参数中配置versionKey: false
  • mongoose总是会在保存文档时(save)触发验证,如果希望禁用这种行为,可以有两种做法:

    • Schema的第二个参数中配置validateBeforeSave:false,这将导致使用该SchemaModel在保存时均不会触发验证
    • 在调用save方法或create方法时,传入一个配置对象,配置validateBeforeSave:false,这样一来,仅针对这一次调用不进行验证。当给create方法传入配置时,为了避免歧义,需要将第一个参数设置为数组。 如果是对象,可能会认为是传递的第二个对象,而可能不是配置。

      mongoose支持<Model>.validate(doc, [context])直接对文档进行验证,该验证是异步的。

      当验证失败时,抛出的错误类型是ValidationError

      注意:unique 在数据库中表现为唯一索引(unique index),并不属于验证的范畴,因此尽管unique约束不满足,也不会导致验证失败,最终在添加时会抛出MongoError,而不是ValidationError

  • <Model>.create(doc, option)等效于new <Model>(doc).save(option)

    • 如果你给create传入的是多个文档,则其在内部会创建多个模型,然后循环调用它们的save方法
  • 无论用哪一种方式,都会得到模型实例,该实例会被mongoose持续跟踪,只要对模型实例的修改都会被记录,一旦重新调用模型实例的save方法,就会把之前对模型的所有更改持久化到数据库。

  • 新增对象时,如果遇到Schema中没有定义的字段,则会被忽略

文档查询

补充

  1. mongoodb操作和mongoose操作对比

  1. mongodb的备份与恢复
# 恢复
mongorestore -d <dbname> <backupDir>
mongorestore -d test ./backup/test
# 备份
mongodump -d <dbname> -o <backupDir>

mongodb 原生查询

// 根据条件、投影查询指定集合,返回游标
db.<collection>.find([filter], [projection]);

db.users.find()

返回结果

查询返回的是一个游标对象,它类似于迭代器,可以在查询结果中进行迭代

cursor的成员:

  • next():游标向后移动,并返回下一个结果,如果没有结果则报错

  • hasNext():判断游标是否还能向后移动,返回boolean

  • skip(n):去前面的n条数据,返回**cursor**

  • limit(n):取当前结果的n条数据,返回**cursor**

  • sort(sortObj):按照指定的条件排序,返回**cursor** 生序 1 降序-1

  • count():得到符合filter的结果数量,返回Number

  • size():得到最终结果的数量,返回Number

由于某些函数会继续返回cursor,因此可以对其进行链式编程,返回cursor的函数成为了链中的一环,无论它们的调用顺序如何,始终按照下面的顺序执行:

sort -> skip -> limit

查询条件

find函数的第一个参数是查询条件filter,它的写法极其丰富,下面列举了大部分情况下我们可能使用到的写法。

// 查询所有 name="曹敏" 的用户
{
  name: "曹敏"
}

// 查询所有 loginId 以 7 结尾 并且 name 包含 敏 的用户
{
  loginId: /7$/ ,
 	name: //
}

// 查询所有 loginId 以 7 结尾 或者 name 包含 敏 的用户
{
  $or: [
    {
      loginId: /7$/,
    },
    {
      name: //
    },
  ],
}

// 查询所有年龄等于18 或 20 或 25 的用户
{
  age: {
    $in: [18, 20, 25]
  }
}

// 查询所有年龄不等于18 或 20 或 25 的用户
{
  age: {
    $nin: [18, 20, 25]
  }
}

// 查询所有年龄在 20~30 之间的用户
{
  age: {
    $gt: 20,
    $lt: 30
  }
}

查询中出现了一些特殊的属性,它以$开头,表达了特殊的查询含义,这些属性称之为操作符 operator

查询中的常用操作符包括:

  • $or:或者

  • $and:并且

  • $in:在…之中

  • $nin:不在…之中

  • $gt:大于

  • $gte:大于等于

  • $lt:小于

  • $lte:小于等于

  • $ne:不等于

    练习

属性路径

投影

find中的第二个参数projection表示投影,类似于mysql中的select

它是一个对象,表达了哪些字段需要投影到查询结果中,哪些不需要

// 查询结果中仅包含 name、age,以及会自动包含的 _id
{
  name: 1,
  age: 1
}

// 查询结果不能包含 loginPwd、age,其他的都要包含
{
  loginPwd: 0,
  age: 0
}

// 查询结果中仅包含 name、age,不能包含_id
{
  name: 1,
  age: 1,
  _id: 0
}

// 错误:除了 _id 外,其他的字段不能混合编写
{
  name: 1,
  age: 0
}

mongoose 中的查询

<Model>.findById(id); // 按照id查询单条数据
<Model>.findOne(filter, projection); // 根据条件和投影查询单条数据
<Model>.find(filter, projection); // 根据条件和投影查询多条数据

findOnefind如果没有给予回调或等待,则不会真正的进行查询,而是返回一个DocumentQuery对象,可以通过DocumentQuery对象进行链式调用进一步获取结果,直到传入了回调、等待、调用exec时,才会真正执行。

链式调用中包括:

  • count
  • limit
  • skip
  • sort

差异点

  1. count得到的是当前结果的数量

  2. 查询id时,使用字符串即可。不需要 ObjectId

  3. projection支持字符串写法

  4. sort支持字符串写法

  5. populate支持关联查询

mongodb 适合读写大量数据,不适合复杂关系。不在乎数据冗余

文档更新

mongodb 原生

// 根据查询条件,更新结果中的第一篇文档,更新的内容由update参数决定,options决定更新的一些细节
db.<collection>.updateOne(filter, update, [options]);

// 根据查询条件,更新所有结果,更新的内容由update参数决定,options决定更新的一些细节
db.<collection>.updateMany(filter, update, [options]);

过滤条件

同查询一致

更新内容

第二个参数决定了更新哪些字段,它的常见写法如下:

  1. 字段操作
// 将匹配文档的 name 设置为 邓哥,address.city 设置为 哈尔滨
{
  $set: { name:"邓哥", "address.city": "哈尔滨" }
}

// 将匹配文档的 name 设置为 邓哥,并将其年龄增加2
{
  $set: { name:"邓哥" },
  $inc: { age: 2 }
}

// 将匹配文档的 name 设置为 邓哥,并将其年龄乘以2
{
  $set: { name:"邓哥" },
  $mul: { age: 2 }
}

// 将匹配文档的 name 字段修改为 fullname
{
  $rename: { name: "fullname" }
}

// 将匹配文档的 age 字段、address.province 字段 删除
{
  $unset: {age:"", "address.province":""}
}
  1. 数组操作
// 向 loves 添加一项:秋葵
// 若数组中不存在则进行添加 若存在则不进行任何操作
{
  $addToSet: {
    loves: "秋葵";
  }
}

// 向 loves 添加一项:秋葵
// 无论数组中是否存在,都必定会添加
{
  $push: {
    loves: "秋葵";
  }
}

// 向 loves 添加多项:秋葵 香菜
{
  $push: {
    loves: {
      $each: ["秋葵", "香菜"];
    }
  }
}

// 删除 loves 中满足条件的项: 是秋葵 或 香菜
{
  $pull: {
    loves: {
      $in: ["秋葵", "香菜"];
    }
  }
}

// 将所有loves中的 其他 修改为 other
// 该操作符需要配合查询条件使用
db.users.updateOne(
  {
    loves: "其他",
  },
  {
    $set: {
      "loves.$": "other",
    },
  }
);

更多的操作符见:https://docs.mongodb.com/manual/reference/operator/update/

其他配置

第三个参数是其他配置

  • upsert:默认false,若无法找到匹配项,则进行添加

mongoose

方式 1:在模型实例中进行更新,然后保存

const u = await User.findById("5ed093872e3da2b654983476");
u.address.province = "黑龙江";
u.loves.push("秋葵", "香菜");
await u.save(); // 此时会自动对比新旧文档,完成更新

方式 2:直接使用函数进行更新

<Model>.updateOne(filter, doc, [options]);
<Model>.updateMany(filter, doc, [options]);

这种方式与原生的区别:

  • _id可以直接使用字符串进行匹配
  • doc中可以省略$set,直接更改即可
  • 默认情况下,不会触发验证,需要在options设置runValidators: true开启验证

删除文档

mongodb 原生

db.<collection>.deleteOne(filter)
db.<collection>.deleteMany(filter)

mongoose

<Model>.deleteOne(filter);
<Model>.deleteMany(filter);

索引

索引的概念

在数据库中,索引类似于一个目录,用于快速定位到具体的内容

使用索引可以显著的提高查询效率,但会增加额外的存储空间

无索引的查询:

带索引的查询:

mongodb 中的索引操作

创建索引

// 为某个集合创建索引
db.<collection>.createIndex(keys, [options]);
  • keys:指定索引中关联的字段,以及字段的排序方式,1 为升序,-1 为降序
// 索引关联 age 字段,按照升序排序
{
  age: 1;
}
  • options索引的配置
    • background:默认false,建索引过程会阻塞其它数据库操作,是否以后台的形式创建索引
    • unique:默认false,是否是唯一索引
    • name:索引名称

在 mongodb 中,索引的存储结构是 B-树

其他索引操作

// 查看所有索引
db.<collection>.getIndexes()
// 查看集合索引占用空间
db.<collection>.totalIndexSize()
// 删除所有索引
db.<collection>.dropIndexes()
// 删除集合指定索引
db.<collection>.dropIndex("索引名称")

最佳实践

  • 针对数据量大的集合使用索引
  • 针对常用的查询或排序字段使用索引
  • 尽量避免在程序运行过程中频繁创建和删除索引

mongoose 中的并发管理

当多个异步函数同时操作数据库时,就可能发生这样的情况,具体的场景往往发生在并发请求中

面对这种情况,mongoose作出以下假设:

  • 当修改一个文档时,如果某些字段已经不再和数据库对应,说明这个字段的数据是脏数据(dirty data),对于脏数据,不应该对数据库产生影响
  • 当修改一个文档时,如果字段和数据库是对应的,则是正常数据,正常数据可以正常的更改数据库

因此,mongoose对于上述场景的处理如下:

可以看出,对于user2的修改,nameloginId是脏数据,因此不会更新到数据库中,而age是正常数据,对它的更改会应用到数据库

然而,mongoose无法准确的判定对数组是否是脏数据,因此,如果遇到数组的修改,mongoose会做出如下处理:

  • 当新增文档时,会自动添加字段__v,用于记录更新版本号,一开始为0
  • 通过模型实例对数组进行修改后,保存时会在内部调用实例的increment函数,将版本号+1
  • 当其他模型实例也更改了数组,保存时会对比版本号,如果不一致,则会引发VersionError

原理图如下:

出现错误是好事,可以提醒开发者:这一次保存的某些数据是脏数据,应该引起重视。开发者可以灵活的根据具体情况作出处理,比如提示用户保存失败,或者重新获取数据然后保存。

由于mongoose仅针对数组进行版本控制,如果要针对所有字段都进行版本控制,需要使用mongoose的插件:mongoose-update-if-current

插件地址:https://github.com/eoin-obrien/mongoose-update-if-current

该插件有一个bug,需要手动解决

使用插件后,所有的字段都将受到版本控制,一旦版本不一致,将引发VersionError

mongodb 的分布式架构

虚拟属性

new Schema({
  firstName: {
    type: String,
  },
  lastName: {
    type: String,
  },
  fullName: {
    virtual: true, // 虚拟属性,不会持久化到数据库
    get() {
      // 读取该属性时运行的函数
      return this.firstName + " " + this.lastName;
    },
  },
});

模型方法

var schema = new Schema({});
schema.methods.methodName = fn; // 添加实例方法
schema.static("methodName", fn); // 添加静态方法

文章作者: Sunny
版权声明: 本博客所有文章除特別声明外,均采用 CC BY 4.0 许可协议。转载请注明来源 Sunny !
  目录