面向Node.js开发者的MongoDB教程 (1):MongoDB和Mongoose基础

3,625 阅读8分钟

教程说明

本文是《面向Node.js开发者的MongoDB教程》的第一章,本教程大部分参考了《MongoDB权威指南(第二版)》,Kirstina Chodorow著(O'Reilly, 2013)。可权威指南中使用的MongoDB版本是2.4.x的。在实际写用户代码时,发现有些语法已经不可用了,本教程所使用的环境中MongoDB版本为5.0.x的最新版。并结合我自己是一名Node.js开发者,教程中应用内代码都是使用的Node.js + Mongoose编写。 ​

安装MongoDB

windows/Linux/MacOS等各个环境MongoDB的安装大家可以在MongoDB官网-《MongoDB社区下载》自行下载安装。本文我更加倾向于读者尝试使用Docker安装MongoDB,理由有两个: ​

1. 简单: 只需要使用一行命令,就可以通过镜像运行一个MongoDB数据库的容器

2. 易于调试: 教程中可能需要对同一份数据进行增删改查,使用Docker可以创建多个互不干扰的容器,方便调试对比。

具体步骤可以参考本文末尾部分《使用Docker安装MongoDB》。

数据来源(B站热点)

image.png

本次教程中数据库的数据,来源来自B站的“综合热门”的后端接口数据。笔者提供了25300条文档数据,方便之后的教程中用来进行数据的增删改查。下面取了一条数据库中的数据:

{
    "_id" : ObjectId("6123b52a64ceb40e2917c933"),
    "videos" : 1,
    "tid" : 17,
    "desc" : "",
    "duration" : 1537,
    "aid" : 675061011,
    "tname" : "单机游戏",
    "copyright" : 1,
    "pic" : "http://i1.hdslb.com/bfs/archive/7b9bff6a7e31bc55fc6a80341a23c43225b5f24e.jpg",
    "title" : "三界四洲不可救【龙崎黑神话解析03】",
    "pubdate" : ISODate("1970-01-19T20:41:24.902Z"),
    "ctime" : ISODate("1970-01-19T20:41:24.902Z"),
    "state" : 0,
    "owner" : {
        "mid" : 4564056,
        "name" : "龙崎棒棒糖",
        "face" : "http://i0.hdslb.com/bfs/face/74b6d0663e92c0595c40033a1f3495aff7d27f4b.jpg"
    },
    "stat" : {
        "aid" : 675061011,
        "view" : 1255439,
        "danmaku" : 8719,
        "reply" : 6384,
        "favorite" : 30489,
        "coin" : 81238,
        "share" : 12405,
        "now_rank" : 0,
        "his_rank" : 46,
        "like" : 99243,
        "dislike" : 0
    },
    "dynamic" : "原本写了8000字文案,结果把自己累出病来了,就删了一千多字,途中电脑崩溃了一次,把写了一个半小时的文案和其他保存文件给搞没了,可以说是我有史以来最敢的一期视频,技术部分感谢沙滩大佬的传授",
    "cid" : 395060770,
    "dimension" : {
        "width" : 1920,
        "height" : 1080,
        "rotate" : 0
    },
    "short_link" : "https://b23.tv/BV18U4y1j765",
    "short_link_v2" : "https://b23.tv/BV18U4y1j765",
    "first_frame" : "http://i0.hdslb.com/bfs/storyff/n210823a22pk2invww0sv02xkt574ew8_firsti.jpg",
    "bvid" : "BV18U4y1j765",
    "season_type" : 0,
    "rcmd_reason" : {
        "content" : "百万播放",
        "corner_mark" : 0
    },
    "update_time" : ISODate("2021-08-23T14:48:10.392Z"),
    "__v" : 0
}

大家可以通过百度网盘下载我提供的数据文件bilihot.json,并导入到MongoDB中。

百度网盘下载地址:

使用mongoimport导入bilihot.json:

$ mongoimport -d bilibili_hot -c hotspots --file /bilihot.json --type json

参数说明:

  • -d: 数据库名称
  • -c: 集合名称
  • --file: 要导入的文件路径
  • --type: 导入的文件类型,默认为json

建议使用云服务器

对于大部分前端开发者而言,不仅建议在本地跑一遍相关代码,更加建议自己购买一个云服务器,安装MongoDB后使用远程命令行连接运行一遍。这样有利于学习Linux相关命令、适应后端运行环境。 ​

950-390.jpg 阿里云-云服务器最新优惠活动地址

MongoDB基础

数据库

在MongoDB中,多个文档组成集合,而多个集合可以组成数据库。一个MongoDB实例可以承载多个数据库,每个数据库拥有0个或者多个集合。每个数据库都有独立的权限,即便是在磁盘上,不同的数据库也放置在不同的文件中。 ​

在MongoDB中,我们可以使用use db_name来切换数据库,例如本教程中我们的数据库名称为bilibili_hot,那么你可以通过以下命令进入到该数据库。

> use bilibili_hot

文档

文档(document)是MongoDB中的核心概念,如果你熟悉MySQL等关系型数据库,你可以将文档比喻为一行。文档在JavaScript文档被表示为为对象,例如刚才存入数据库中的一条B站热点数据就是一个文档:

{
  "title" : "三界四洲不可救【龙崎黑神话解析03】"
  "pubdate" : ISODate("1970-01-19T20:41:24.902Z"),
  "ctime" : ISODate("1970-01-19T20:41:24.902Z"),
  "duration" : 1537,
  ...
}

MongoDB的文档区分类型和大小写,并且不能有重复的key,这和JavaScrit中对象的概念基本一致,对于Node.js开发者来说很好理解。例如下面两个文档就是不同的:

{
  "title" : "三界四洲不可救【龙崎黑神话解析03】"
}

{
  "Title" : "三界四洲不可救【龙崎黑神话解析03】"
}

下面两个文档也是不同的:

{
	"duration" : 1537
}

{
	"duration" : "1537"
}

集合

集合(collection)就是一组文档,一个或多个集合组成了一个MongooDB的数据库,我们可以将集合比喻为关系型数据库中的一张表。

集合的英文单词是collection,这个单词在MongoDB的操作中经常会使用到:

  • 例如显示所有的集合:
> show collections
  • 创建集合statistics
> db.createCollection('statistics')
  • 查询hotspots集合中的全部文档数据:
> db.getCollection('hotspots').find({})

MongoDB中的集合是动态模式,这意味着集合中的文档的结构和类型可以是各式各样的,下面两个文档是可以放在同一个集合中,一个文档存的是视频的名称,一个文档存的是视频的播放次数。

{ "title": "三界四洲不可救【龙崎黑神话解析03】" },
{ "view": 1280 }

上面的两个文档,不仅是值的类型不一致,同时键也不一致。作为一名有经验的程序员,肯定不会在集合中这样去组织文档。针对上面的的情况,我们建议使用不同的集合去存储视频的基本信息和视频的统计信息(播放数、点赞数、投币数等)

mongod

启动MongoDB 在命令行执行mongod,即可启动MongoDB服务器,mongod在启动时可以使用许多可配置项,下面列举几个常用的:

$ mongod
  • --dbpath: 指定一个目录作为数据目录,其默认目录为/data/db
  • --port: 指定服务器监听的端口号,mongod默认占用27017端口
  • --fork:调用fork创建子进程,在后台运行MongoDB。启用--fork时,必须同时启用--logpath
  • --logpath:指定文件作为输出信息的文件,而非在命令行上输出。
  • --config:额外加载配置文件,未在命令行中指定的选项将使用配置文件中的从参数。

停止MongoDB 关闭运行中的服务器的命令是一个管理员命令,首先我们需要在Mongo Shell中切换到admin数据库。

> use admin

再使用db.shutdownServer()

> db.shutdownServer()

mongo

在命令行执行mongo,可以启动mongosh(又称MongoDB Shell),它是我们学习MongoDB最重要的工具,绝大多数的MongoDB操作都需要使用shell完成。在运行mongo前,请先执行mongod运行MongoDB服务器,之后shell将自动连接到MongoDB服务器。

$ mongo

MongoDB server version: 5.0.2
---
> 

shell是一个独立的MongoDB客户端,启动时,shell会连接到MongoDB服务器的test数据库我们可以使用db命令查看当前使用了哪个数据库:

  • 查看当前数据库db
> db
test
  • 查看目前所有的数据库show dbs
> show dbs
admin         0.000GB
bilibili_hot  0.015GB
config        0.000GB
local         0.000GB
  • 使用某一个数据库use 数据库名, 例如使用bilibili_hot数据库
> use bilibili_hot
switched to db bilibili_hot

CRUD

创建文档

insert()方法用于把文档插入集合中,我们把一条数据插入到hotspots集合下

> db.hotspots.insert({
    videos: 1,
    tid: 17,
    desc: "",
    duration: 1537,
    aid: 675061011,
    tname: "单机游戏",
    copyright: 1,
    pic: "http://i1.hdslb.com/bfs/archive/7b9bff6a7e31bc55fc6a80341a23c43225b5f24e.jpg",
    title: "三界四洲不可救【龙崎黑神话解析03】(新增)",
    pubdate: new Date("1970-01-19T20:41:24.902Z"),
    ctime: new Date("1970-01-19T20:41:24.902Z"),
    state: 0,
    owner: {
      mid: 4564056,
      name: "龙崎棒棒糖",
      face: "http://i0.hdslb.com/bfs/face/74b6d0663e92c0595c40033a1f3495aff7d27f4b.jpg",
    },
    stat: {
      aid: 675061011,
      view: 1255439,
      danmaku: 8719,
      reply: 6384,
      favorite: 30489,
      coin: 81238,
      share: 12405,
      now_rank: 0,
      his_rank: 46,
      like: 99243,
      dislike: 0,
    },
    dynamic:
      "原本写了8000字文案,结果把自己累出病来了,就删了一千多字,途中电脑崩溃了一次,把写了一个半小时的文案和其他保存文件给搞没了,可以说是我有史以来最敢的一期视频,技术部分感谢沙滩大佬的传授",
    cid: 395060770,
    dimension: {
      width: 1920,
      height: 1080,
      rotate: 0,
    },
    short_link: "https://b23.tv/BV18U4y1j765",
    short_link_v2: "https://b23.tv/BV18U4y1j765",
    first_frame:
      "http://i0.hdslb.com/bfs/storyff/n210823a22pk2invww0sv02xkt574ew8_firsti.jpg",
    bvid: "BV18U4y1j765",
    season_type: 0,
    rcmd_reason: {
      content: "百万播放",
      corner_mark: 0,
    },
    update_time: new Date("2021-08-23T14:48:10.392Z")
 })

查询文档

find()方法用于查询集合中的文档,如果想查询一个文档,可用findOne()。我们来找到刚才新增的那一个文档:

> db.hotspots.findOne({ title: '三界四洲不可救【龙崎黑神话解析03】(新增)' })

修改文档

update()方法用来修改文档,我们来改变刚才新增的那个文档的title字段:

> db.hotspots.update(
  { title: "三界四洲不可救【龙崎黑神话解析03】(新增)" },
  { $set: { title: "三界四洲不可救【龙崎黑神话解析03】(最新新增)" } }
)

删除文档

remove()方法用来将文档从集合中永久移除,我们来把刚才更改过的那条文档给删除掉:

> db.hotspots.remove({ title: '三界四洲不可救【龙崎黑神话解析03】(最新新增)' })

Mongoose基础

Mongoose是在node环境中MongoDB数据库操作的封装,一个对象模型工具。将数据库中的数据转换为JavaScript对象以方便我们在应用中使用。

安装Mongoose

npm install mongoose --save

Connection

你可以使用mongoose.connect()方法连接到MongoDB:

const mongoose = require("mongoose");

mongoose
  .connect("mongodb://127.0.0.1:27019/bilibili_hot", { useNewUrlParser: true })
  .then(() => console.log("MongoDB Connected"))
  .catch((err) => console.log(err));

该方法的第1个参数是MongoDB的连接地址,第2个参数是一个options对象,该对象会被Mongoose传递给MongoDB的驱动程序,你可以在《MongoDB Node.js驱动程序文档》中找到完成的options的配置项, 下面是一些比较重要的配置项:

  • user/pass: 用于认证的用户名和密码。Mongoose 特有,等价于 MongoDB 驱动的auth.userauth.password选项。
  • useFindAndModify:默认为true,设置为false会让findOneAndUpdate()findOneAndRemove()使用findOneAndUpdate()而不是findAndModify()
  • poolSize:MongoDB 驱动程序将为此连接保持打开的最大套接字数。默认情况下,poolSize是5。
  • useNewUrlParser:底层 MongoDB 驱动程序已弃用其当前的连接字符串解析器。因为这是一个重大变化,他们添加了一个useNewUrlParser标志,允许用户在发现新解析器中的错误时回退到旧解析器。你应该设置,useNewUrlParser: true

Schema

Schema是Mongoose中的重要概念,Schema定义了集合中文档的结构。结合上文提到的数据库的数据,我们定义的Schema如下所示:

// HotSpot.js

const mongoose = require("mongoose");
const Schema = mongoose.Schema;

const hotSpotSchema = new Schema({
  aid: {
    type: Number,
    required: true,
  },
  videos: {
    type: Number,
    default: 1,
  },
  tid: {
    type: Number,
    default: 0,
  },
  tname: {
    type: String,
    require: true,
  },
  copyright: {
    type: Number,
    dafault: 1,
  },
  pic: {
    type: String,
  },
  title: {
    type: String,
    require: true,
  },
  pubdate: {
    type: Date,
  },
  ctime: {
    type: Date,
    require: true,
  },
  desc: {
    type: String,
    default: "",
  },
  state: {
    type: Number,
  },
  duration: {
    type: Number,
    default: 0,
  },
  owner: {
    mid: {
      type: Number,
    },
    name: {
      type: String,
    },
    face: {
      type: String,
    },
  },
  stat: {
    aid: {
      type: Number,
    },
    view: {
      type: Number,
    },
    danmaku: {
      type: Number,
    },
    reply: {
      type: Number,
    },
    favorite: {
      type: Number,
    },
    coin: {
      type: Number,
    },
    share: {
      type: Number,
    },
    now_rank: {
      type: Number,
    },
    his_rank: {
      type: Number,
    },
    like: {
      type: Number,
    },
    dislike: {
      type: Number,
    },
  },
  dynamic: {
    type: String,
  },
  cid: {
    type: Number,
  },
  dimension: {
    width: {
      type: Number,
    },
    height: {
      type: Number,
    },
    rotate: {
      type: Number,
    },
  },
  short_link: {
    type: String,
  },
  short_link_v2: {
    type: String,
  },
  first_frame: {
    type: String,
  },
  bvid: {
    type: String,
  },
  season_type: {
    type: Number,
  },
  rcmd_reason: {
    content: {
      type: String,
    },
    corner_mark: {
      type: Number,
    },
  },
  update_time: {
    type: Date,
  },
});

Model

Model是从Schema编译来的构造函数,这也就是为什么这里需要使用大写字母开头的原因。Model负责从MongoDB中创建和读取文档。

当你调用mongoose.model()是时,Mongoose就会为你编译一个Model

const HotSpot = mongoose.model("hotSpot", hotSpotSchema);
  • 参数1: 是collection的单数名称,也就是说,当你的数据库中没有任何集合的时候,当你使用Mongoose连接到MongoDB,并准备插入数据时,Mongoose会自动为你创建一个叫hotspots的集合。
  • 参数2: 调用new Schema所创建的Schema

CRUD

创建好Model后,你就可以使用Model对应的方法操作集合中的文档了,这里我们简单的介绍一下使用Mongoose中的CRUD方法。

创建文档

你可以使用Model.create() 创建一条文档数据:

 const newVideo = {
    videos: 1,
    tid: 17,
    desc: "",
    duration: 1537,
    aid: 675061011,
    tname: "单机游戏",
    copyright: 1,
    pic: "http://i1.hdslb.com/bfs/archive/7b9bff6a7e31bc55fc6a80341a23c43225b5f24e.jpg",
    title: "三界四洲不可救【龙崎黑神话解析03】(新增)",
    pubdate: new Date("1970-01-19T20:41:24.902Z"),
    ctime: new Date("1970-01-19T20:41:24.902Z"),
    state: 0,
    owner: {
      mid: 4564056,
      name: "龙崎棒棒糖",
      face: "http://i0.hdslb.com/bfs/face/74b6d0663e92c0595c40033a1f3495aff7d27f4b.jpg",
    },
    stat: {
      aid: 675061011,
      view: 1255439,
      danmaku: 8719,
      reply: 6384,
      favorite: 30489,
      coin: 81238,
      share: 12405,
      now_rank: 0,
      his_rank: 46,
      like: 99243,
      dislike: 0,
    },
    dynamic:
      "原本写了8000字文案,结果把自己累出病来了,就删了一千多字,途中电脑崩溃了一次,把写了一个半小时的文案和其他保存文件给搞没了,可以说是我有史以来最敢的一期视频,技术部分感谢沙滩大佬的传授",
    cid: 395060770,
    dimension: {
      width: 1920,
      height: 1080,
      rotate: 0,
    },
    short_link: "https://b23.tv/BV18U4y1j765",
    short_link_v2: "https://b23.tv/BV18U4y1j765",
    first_frame:
      "http://i0.hdslb.com/bfs/storyff/n210823a22pk2invww0sv02xkt574ew8_firsti.jpg",
    bvid: "BV18U4y1j765",
    season_type: 0,
    rcmd_reason: {
      content: "百万播放",
      corner_mark: 0,
    },
    update_time: new Date("2021-08-23T14:48:10.392Z")
  };


  HotSpot.create(newVideo, function (err, video) {
    if (err) {
      console.log(err);
      return;
    }
    console.log(video)
  });

查询文档

查询的方法有很多,目前我们只介绍Model.find(),让我们来查询刚才新增的那一条数据吧:

HotSpot.find({ title: '三界四洲不可救【龙崎黑神话解析03】(新增)'}, function(err, videos) {
  if (err) {
    console.log(err)
    return
  }
  console.log(videos)
})

修改文档

我们可以使用Model.updateOne()更新数据库中的文档

HotSpot.updateOne(
  { title: "三界四洲不可救【龙崎黑神话解析03】(新增)" },
  { $set: { title: "三界四洲不可救【龙崎黑神话解析03】(最新新增)" } },
  function (err, raw) {
    if (err) {
      console.log(err);
      return;
    }
    console.log(raw);
  }
);

删除文档

让我们使用Model.deleteOne()删除刚才更改的文档吧,把我们的数据库恢复原貌:

const raw = await HotSpot.deleteOne({ title: "三界四洲不可救【龙崎黑神话解析03】(最新新增)" });

console.log(raw)

Mongoose总结

在想使用Mongoose来对MongoDB进行操作前,需要先进行三步操作,分别是:

  • 连接MongoDB
  • 生成Schema
  • 生成Model

然后在Model中可以进行相应的CRUD操作

const mongoose = require("mongoose");
const hotSpotSchema = new Schema({...}) // 生成Schmea
const HotSpot = mongoose.model("hotSpot", hotSpotSchema); // 生成Model

// 连接MongoDB
mongoose
  .connect("mongodb://127.0.0.1:27019/bilibili_hot", { useNewUrlParser: true })
  .then(() => console.log("MongoDB Connected"))
  .catch((err) => console.log(err));

// 数据库CRUD操作
HotSpot.findOne({ ... })

其他

1. 使用Docker安装MongoDB

运行MongoDB容器 使用下面命令运行一个mongo容器,如果你本地没有找到mongo的镜像,那么docker会自动拉取远程镜像,可能会耗时比较长。

$ docker run -p 27019:27017 --name mymongo -v ~/db:/data/db -d mongo

参数说明:

  • -p: 端口映射,-p 27019:27017是指将容器内27017端口映射到宿主机的27019端口
  • --name:容器命名,--name mymongo 将该运行中的容器命名为mymongo
  • -v: 数据卷,-v ~/db:/data/db 将容器内存放数据的/data/db目录映射到宿主机的~/db目录上

查看容器运行状态 使用docker ps命令查看容器的运行状态

$ docker ps 

CONTAINER ID   IMAGE     COMMAND                  CREATED         STATUS         PORTS                                           NAMES
ddfc4fd875e6   mongo     "docker-entrypoint.s…"   4 minutes ago   Up 4 minutes   0.0.0.0:27019->27017/tcp, :::27019->27017/tcp   mymongo

进入容器内并进入bash操作 我们可以使用docker exec进入容器内,调式MongoDB。需要注意的是,ddfc4fd875e6是上一步执行docker ps命令显示的CONTAINER ID,读者的值肯定和笔者的不一致,需替换成自己的CONTAINER ID

$ docker exec -it ddfc4fd875e6 /bin/bash

root@ddfc4fd875e6:/# 

上面表示已经进入到了容器内部,我们来查看一下MongoDB的版本,在命令行输入mongo进入MongoDB shell。

root@ddfc4fd875e6:/# mongo

此时进入到了MongoDB shell中,我们就可以执行相应操作了

# 查看MongoDB版本
> db.version()
5.0.2

# 显示数据库
> show dbs
admin   0.000GB
config  0.000GB
local   0.000GB

退出shell和容器

# 输入exit回车 退出MongoDB shell
> exit
bye

# 输入exit回车,退出容器
root@ddfc4fd875e6:/# exit
exit

2. 参考文献

  1. Mongoose官网
  2. MongoDB官网