MongoDB 4.2基本用法以及Mongoose的使用(长文)

一、前言

本来在前言里面写了一堆数据库类型介绍、为什么要学习Mongodb数据库等等,后来我又全删了,这种概念的东西看了也只能混个脸熟,即使明白了也过一阵子忘记了,只有真正用起来了才逐渐明白这些概念,如果刚一开始接触太多概念容易让人迷惑(因为我就这样…),实践出真知嘛。

二、准备

  1. MongoDB 数据库,点我下载
  2. Studio 3T 可视化工具,点我下载
  3. Node.js 环境,点我下载

说一点,这篇文章重点是教你如何快速入门MongoDB,至于如何去安装它们不会花篇幅描述。

这里有一份官方的安装MongoDB的指南Install MongoDB,不过是英文的,你可以使用谷歌翻译。

另外一份官方文档,使用MongoDB Shell 连接数据库,mongo shell。

下面是我所安装的版本:

  • MongoDB 4.2
  • Studio 3T 2019.7.0
  • Node 12.8.1

此外,我接下来所讲解的操作都是在MongoDB Shell 环境下进行的,如果需要在其他环境下进行需要安装或引入相关的驱动。当然为了方便,我在可视化工具下进行操作,因为可视化工具集成了MongoDB Shell 环境。

三、基本概念

虽然我提倡不接触那些晦涩难搞的概念,比如事务,但接下来的概念你还是需要知道的!

3.1 Collection和Document

学过SQL的同学应该知道,进行数据库操作有以下操作(略过登陆权限啥的):

  • 创建数据库
  • 建立视图
  • 建表
  • CRUD(增删改查)

上面每一步都需要输入相关的SQL脚本去执行。但是在MongoDB中不需要主动去创建数据,也不需要去建“表”,你只要直接在“表”中插一条数据,那么MongoDB会帮你自动创建数据库和“表”,这是动态进行的,很灵活。这个“表”就是MongoDB中的集合(collection),表中每一条数据都被称为文档。从这我们可以看出来,关系型数据库和非关系型数据库之间的一个区别了吧(当然我还是不会说太多)。

一张图来说明MongoDB数据库集合文档之间的关系:

MongoDB 4.2基本用法以及Mongoose的使用(长文)_第1张图片

  • DBMS:对应多个数据库
  • Collection:一个数据库可以有很多集合(你也可以理解为“表”)
  • Document:一个集合有多个文档(可以理解为一张表有很多数据项)

3.2 BSON

BSON是一种二进制序列化格式,用于在MongoDB中存储文档和进行远程过程调用。 BSON规范位于bsonspec.org

它的数据结构是一种类似JSON的数据结构,可以存储各种类型数据,比如二进制数据图像、视频等。特点如下:

  • 自动识别类型
  • 自动指定_id字段设置为主键,保证不重复
  • 动态添加属性
  • BSON字符串为UTF-8

搞前端的同学肯定明白了,这和JS的灵活性很像啊。连字段都可以动态加入,这也太牛逼了。

四、MongoDB操作

4.1 基本操作

在平时业务中,我们大部分的操作都是CRUD操作,在此之前,先介绍几个基本操作,打开你的MongoDB Shell或者 Studio 3T点击Intelli Shell 都是一样的(个人推荐后者)。

MongoDB 4.2基本用法以及Mongoose的使用(长文)_第2张图片

4.1.1 显示所有数据库

show dbs

4.1.2 显示当前数据库

db

4.1.3 显示所有集合

show collections

4.1.4 删除某个集合

db..drop()

4.1.5 删除数据库

db.dropdatabase()

4.1.6 使用某个数据库

use 

4.2 CRUD操作

上面多次提到了这个CRUD,意思我们都知道了,英文单词就是下面四个:

  • Create:创建文档
  • Read:读取文档
  • Update:更新文档
  • Delete:删除文档

可以说这一小节无疑是最重要的一部分了,上面也提到过了,我们的业务大部分就是增删改查操作了。下面讲解这些命令的时候会有一部分是与SQL的对比,这一部分来自官网(看了半天的英文…)。

以下示例均在数据库my_test,集合students中进行。

4.2.1 插入/创建 文档

(1) 插入一个或者多个文档
db.collection.insert(
   <文档或文档数组>,
   {
     writeConcern:<文档>,
     ordered: 
   }
)

该方法接受两个参数,第一个参数就是要插入的文档,可以是一个对象表示插入一个文档,也可以是一个对象数组表示插入多个文档。第二个参数一般不需要写,是一些插入配置选项。插入后,会返回一个对象来表明插入的状态。返回的对象属性如下:

{
	acknowledged: ,
	_id: Array
}

下面要介绍的方法和上面的是一样的,只不过下面更加有语义化,推荐使用下方的方法。

(2)插入一个文档
db.collection.insertOne(
   <文档>,
   {
     writeConcern:<文档>
   }
)
(3)插入多个文档
db.collection.insertMany(
   <文档或文档数组>,
   {
     writeConcern:<文档>,
     ordered: 
   }
)

比如我现在向students集合中插入几个文档,

db.students.insertMany(
    [
      {
          name: '老曹',
          age: 22,
          gender: '男',
          address: '湘潭',
          like: {
              singers: ['中岛美雪', '黄家驹', '张国荣'],
              books: ['你不知道的JavaScript', '吉他自学三月通', '小孩']
          }
      },
      {
          name: '老罗',
          age: 24,
          gender: '男',
          address: '南宁',
          like: {
              singers: ['泰勒斯威夫特', '杨千嬅'],
              books: ['高等数学', '初级日语', '挪威的森林']
          }
      },
      {
          name: '老陈',
          age: 23,
          gender: '女',
          address: '株洲',
          like: {
              singers: ['陈奕迅', '林宥嘉'],
              books: ['从你的世界路过', '顾城的诗']
          }
      },
      {
          name: '老吴',
          age: 21,
          gender: '女',
          address: '铜仁',
          like: {
              singers: ['张学友', '刘德华', '老曹'],
              books: ['PHP从入门到精通']
          }
      },
    ]
)

使用db.students.find()可以查看刚刚插入的文档:

在这里插入图片描述

4.2.2 读取/查询文档

(1)查询满足条件的所有文档
db.collection.find(
	query: ,
	projection: 
)

query:查询的条件,当不传或者是一个空对象的时候表示查询所有文档。

projection:反射条件,即决定哪些字段显示。字段被设置为1true的时候才显示,反之0或者false不显示。

下面对上面的students集合进行不同条件的查询来模拟大部分应用场景:

  • 查询name等于老曹的文档
db.students.find({ name: '老曹' })

SQL中表示为:

SELECT * FROM students
WHERE name = '老曹'
  • 查询age大于21,likesingers值在黄家驹陈奕迅之中的文档
db.students.find(
    { 
        age: { $gt: 21 },
        'like.singers': { $in: ['黄家驹', '陈奕迅'] }
    }
)

上面引入的$gt$in是一种操作符,表示大于,类似的还有常用的有:$lt$or等等。

like.singers是为了查询嵌套文档而使用的.操作符。

在SQL中表示:

SELECT * FROM students
WHERE age > 21
AND like.singers IN ('黄家驹', '陈奕迅')
  • 查询喜欢PHP入门到精通或者gender的文档
db.students.find(
    { 
        $or: [ { gender: '男' }, { 'like.books': 'PHP从入门到精通' }]
    }
)

在SQL中表示:

SELECT * FROM students
WHERE gender = '男'
OR like.books = 'PHP从入门到精通'

更多用法请查看文档。

(2)查询满足条件的第一个文档(用法和上述一样)
db.collection.findOne(
	query: ,
	projection: 
)

4.2.3 修改/更新文档

(1)更新一个或多个文档
db.collection.update(
   <query>,
   <update>,
   {
     upsert: <boolean>,
     multi: <boolean>,
     writeConcern: <document>,
     collation: <document>,
     arrayFilters: [ <filterdocument1>, ... ],
     hint:  <document|string>        // Available starting in MongoDB 4.2
   }
)

该方法返回一个WriteResult文档,其中包含操作的状态。

常见参数说明如下:

query:查询条件。
update:更新符合query条件的文档。
upsert:默认值为 false。如果设置为true,则在没有文档符合查询条件时创建一个新文档。
multi:默认值为 false。如果设置为true,则更新多个符合query条件的文档。

其他参数不是很常用,所以不说明,有兴趣可以点这里查看更多的参数用法。下面举几个例子来说明该方法的用法。

  • 找到name老吴的人,把他的age增加两岁,并且把gender设置为
db.students.update(
    { name: '老吴' },
    {
        $set: {  gender: '男' },
        $inc: { age: 2 }
    }
)

在SQL中等同于:

UPDATE students
SET gender = '男'
	age = age + 2
WHERE name = '老吴'
  • 找到age不小于22岁的文档,将address字段删除。
db.students.update(
    { age: { $gte: 22 } },
    {
        $unset: {  address: 1 }
    },
    {
        multi: true
    }
)

类似于SQL中的:

ALTER TABLE students
DROP COLUMN address

ALTER TABLE 语句用于在已有的表中添加、删除或修改列。不等同于MongoDB中的删除字段,但是类似)

  • 向喜欢张学友的文档增加一本喜欢的书离散数学
db.students.update(
    { 'like.singers': '张学友' },
    {
        $push: {  'like.books': '离散数学' }
    }
)

说一下上面出现的操作符,以及一些其他常用操作符:

$set:用来更新字段的值
$inc:可以递增数值
$unset:删除某个字段
$push: 向数组添加元素
$pop:像数组移除元素

(2)更新一个文档
db.collection.updateOne(
   <query>,
   <update>,
   {
     upsert: <boolean>,
     multi: <boolean>,
     writeConcern: <document>,
     collation: <document>,
     arrayFilters: [ <filterdocument1>, ... ],
     hint:  <document|string>        // Available starting in MongoDB 4.2
   }
)
(3)更新多个文档
db.collection.updateMany(
   <query>,
   <update>,
   {
     upsert: <boolean>,
     multi: <boolean>,
     writeConcern: <document>,
     collation: <document>,
     arrayFilters: [ <filterdocument1>, ... ],
     hint:  <document|string>        // Available starting in MongoDB 4.2
   }
)

后两个方法语义更强推荐使用,其实还有一些方法也可以更新不够我认为语义更强的还是最后这两种方法,推荐大家使用。

4.2.4 删除文档

(1)删除一个符合条件的第一个文档
db.collection.deleteOne(
   <filter>,
   {
      writeConcern: <document>,
      collation: <document>
   }
)
(2)删除一个符合条件的所有文档
db.collection.deleteMany(
   <filter>,
   {
      writeConcern: <document>,
      collation: <document>
   }
)

用法和其他方法是差不多的,我相信大家也肯定知道怎么使用,只不过个人不推荐使用删除操作,对数据库删除是一个很危险的操作,搞不好你就凉凉了。

看到这里的话,我相信大家肯定对基本的操作已经掌握了,相对于SQL语句来说,MongoDB Shell更加灵活和简单,上手速度很快。

在MongoDB中支持原生Node.js的驱动,你可以在你的项目中使用以下命令来下载该驱动:

npm i mongodb -S

具体api请参考:MongoDB Node.js Driver

不管什么语言的驱动,底层都是支持MongoDB Shell的,所以虽然有了这个驱动后可以对数据库操作了,但毕竟是原生Node,你懂得还是有点缺陷的,所以就出现了基于mongodb封装的模块mongoose。用起来十分方便,不多说,下面告诉大家如何使用。

五、Mongoose

Mongoose是一个对象文档模型(ODM)库,它对Node原生的MongoDB模块进行了进一步的优化封装,并提供了更多的功能。

Moogoose有三大核心概念:SchemaModelDocument,下面我们详细来说。

5.1 Schema

上官网弄个概念先:

Each schema maps to a MongoDB collection and defines the shape of the documents within that collection.

翻译过来就是:每个模式都映射到MongoDB集合并且定义该文档的形状。

说白了,模式就是一种约束。在没有约束的情况下,得益于MongoDB的动态和灵活性,只要给任意一个文档临时添加一个字段后,在其他文档中也会出现该字段,值是空的。如果在一开始就定义好结构,严格控制好每个集合的类型,那以后维护起来也不会很困难。

个人认为和TypeScript中类型定义有点像,还有其他语言中的class类,都严格约束着数据结构。用法如下:

  • 引入mongoose模块中的Schema属性,该属性是一个构造函数。
  • 该构造函数中第一个参数是一个对象,存储着模式的定义,第二个参数是一个option对象,定义与数据库集合的交互。
 new Schema(definition,option)

option常见选项如下:

  • autoIndex
    – 布尔值,开启自动索引,默认true
  • bufferCommands
    – 布尔值,缓存由于连接问题无法执行的语句,默认true
  • capped
    – 集合中最大文档数量
  • collection
    – 指定应用Schema的集合名称
  • id
    – 布尔值,是否有应用于_id的id处理器,默认true
  • _id
    – 布尔值,是否自动分配id字段,默认true
  • strict
    – 布尔值,不符合Schema的对象不会被插入进数据库,默认true

我们给上面的students集合添加一个模式:

const { Schema } = require('mongoose');
const studentSchema = new Schema({
  name: { type: String },
  age: { type: Number },
  gender: {
    type: String,
    default: 'male'
  },
  like: {
    singers: Array,
    books: Array
  }
});

5.2 Model

Model对象作为集合中的所有文档的表示,相当于MongoDB数据库中的集合collection。

创建模型对象需要使用mongoosemodel方法,语法如下:

model(name, [schema], [collection] , [skipInit])

name参数相当于模型的名字,以后可以同过name找到模型。
schema是创建好的模式对象。
collection是要连接的集合名。
skipInit是否跳过初始化,默认是false

一旦把一个Schema对象编译成一个Model对象,你就完全准备好开始在模型中访问、添加、删除、更新和删除文档了。也就是说有了模型以后我们就可以操作数据库了。

给上面的例子创建一个Model,名字为students,加上之前的整个代码如下:

// student

const { Schema, model } = require('mongoose');
const studentSchema = new Schema({
  name: { type: String },
  age: { type: Number },
  gender: {
    type: String,
    default: 'male'
  },
  like: {
    singers: Array,
    books: Array
  }
});

module.exports = model('students', studentSchema);

5.3 Document

Document表示集合中的具体文档,相当于集合中的一个具体的文档

有了Model对象,就相当于已经和数据库的集合建立关系了,可以进行CRUD操作了。我们要做的就是在数据库操作中引入model就可以了。导出的Model其实就是一个构造函数,不用我说你应该也知道有两种方式来操作数据库了吧。

  • 直接调用构造函数的方法,相关API Model API
  • 实例化一个对象,该对象就是一个Document对象,可以调用Document上的方法,相关API Document API

简单就介绍到这了,API的学习大家看文档就好了(我实在顶不住了…)

六、Node项目中如何使用

在此之前,先初始化一个项目test

mkdir test
cd test
npm init -y
npm i mongoose -S

6.1 环境配置

在后端应用中我只模拟了开发环境和生产环境,如果有其他需求的可以自行更改。

package.json设置环境变量NODE_ENV

  "scripts": {
    "dev": "set NODE_ENV=development&&node controller/student/search.js",
    "build": "set NODE_ENV=production && node app.js"
  },

注意:后面的node执行文件是为了启动方便而随便找的文件,你们可以根据项目自行更改。

我暂时没有使用webpack,因为我只是讲数据库,后面如果我完善了我的应用我会给大家单独写一篇文章。

环境变量设置好后,在根目录下创建一个config文件夹,在index.js中根据环境来引入不同的配置:

index.js

/* 
  windows:生产环境 + 开发环境,其他系统默认生产环境
*/

const os = process.env.os;
const mode = os === 'Windows_NT' ? process.env.NODE_ENV : 'production';

module.exports = {
  mode,
  ...(mode === 'development' ? require('./dev.config') : require('./prod.config'))
};

下面是我开发环境的配置,生产环境我暂时没写,如有需要请自行配置。

dev.config.js

// 开发环境配置

const dbOptions = {
  useNewUrlParser: true,
  useUnifiedTopology: true
}

module.exports = {
  // mongodb
  DB_NAME: 'my_test',
  DB_HOST: '127.0.0.1',
  DB_PORT: '27017',
  DB_OPTIONS: dbOptions,
  
  // other configs...
}

配置好后,我们可以来连接数据库啦!

6.2 连接数据库

在根目录下面创建一个libs存放数据库的连接操作,在目录下面创建database.js

引入前面的配置,关于options的说明请点击:Class: MongoClient

database.js

// 创建数据库连接

const mongoose = require('mongoose');
const db = mongoose.connection;
const { DB_NAME, DB_HOST, DB_PORT, DB_OPTIONS} = require('../config');
const uri = `mongodb://${DB_HOST}:${DB_PORT}/${DB_NAME}`;

// 连接数据库
connectMongoDb();

// 监听连接事件
db.once('open', handleConnectOpen);
db.on('error', handleConnectAfterError);

async function connectMongoDb() {
  try {
    await mongoose.connect(uri, DB_OPTIONS);
  } catch (error) {
    handleConnectInitError(error);
  }
}

// 初始化连接错误处理
function handleConnectInitError(error) {
  console.log(error);
}

// 连接后错误处理
function handleConnectAfterError(error) {
  console.log(error);
}

// 成功连接后的处理函数
function handleConnectOpen(error) {
  if(!error) {
    console.log('数据库连接成功~~~')
  }
}

6.2 创建SchemaModel

在根目录创建一个models目录,存放不同model。我们创建一个student.js来举例:

student.js

// student

const { Schema, model } = require('mongoose');
const studentSchema = new Schema({
  name: { type: String },
  age: { type: Number },
  gender: {
    type: String,
    default: 'male'
  },
  like: {
    singers: Array,
    books: Array
  }
});

module.exports = model('students', studentSchema);

6.3 数据库操作

建立一个controller 目录,该目录下是每不同模块的逻辑操作。在该目录下面再建一个student文件夹,存放对student各种CRUD操作,下面以查询所有文档为例:

search.js

require('../../libs/database');
const StudentModel = require('../../models/student');

(async function () {
  let data = await StudentModel.find({});
  console.log(data)
})();

MongoDB 4.2基本用法以及Mongoose的使用(长文)_第3张图片

使用npm run dev 执行 search.js(在package.json里面配置过),就可以成功取到数据库里面的数据了。

说明:数据是提前插进去了,在下一次插入数据的时候就能检测数据格式啦。

最后看一下我整个项目结构:
MongoDB 4.2基本用法以及Mongoose的使用(长文)_第4张图片

有几个是空的,以后大家做项目应该可以用上,我简单说明如下:

  • utils :存放工具模块
  • static:静态资源模块
  • routers:路由模块
  • log:服务器日志
  • app.js :主入口

还有其他的,以后再来补充上吧.

七、总结

这篇文章总体来说还是有很多缺陷,但是写这篇文章让我本身也学到不少知识,今后还有很长的一段路要走,如果你有缘看到这篇长文,希望对你有帮助,加油~

参考

[1] The MongoDB 4.2 Manual
[2] Mongoose v5.7.12

你可能感兴趣的:(数据库)