mongoose学习笔记

196 阅读7分钟

安装mongoose

npm install mongoose --save

typescript:

不需要 单独安装 @types/mongoose,因为从 Mongoose 5.11.0 版本开始,Mongoose 官方已经内置了对 TypeScript 的支持,提供了自己的 index.d.ts 类型定义文件。

nodejs,express项目中使用mongoose

const express = require('express')
const mongoose = require('mongoose')

const app = express()

const DB = 'mongodb://localhost:27017/test'
// 这里的connect方法返回一个promise
mongoose
  .connect(DB)
  .then(() => {
    console.log('MongoDB connection successfully 👍')
  })
  .catch((error) => {
    console.log(error)
  })

const port = 3000
app.listen(port, 'localhost', () => {
  console.log('App running at http://localhost:3000 👍')
})

注意点:

  • mongodb数据库的url比较特殊,他有自己的协议叫mongodb,所以要写成:
mongodb://localhost:27017/test
  • mongoose.connect()函数返回promise,所以我们可以得到成功和拒绝的状态。

开始学习mongoose

首先建一个目录,叫models,可以在这里定义要映射到数据库的代码。因为mongodb不需要提前建表(集合),所以我们在这里想怎么建就怎么建:

user.js

const { Schema, model } = require('mongoose')

/**
 * 定义 Schema:
 * Schema 定义了 MongoDB 集合中文档的结构和数据类型,类似于数据库表的蓝图。
 * 不能直接用来操作数据库,但用于创建 Model。
 */
const userSchema = new Schema(
  {
    username: {
      type: String, // 注意,大写的String,不是typescript中的小写string
      required: true,
      unique: true,
    },
    password: {
      type: String,
      required: true,
    },
    email: String,
  },
  {
    timestamps: true, // 自动添加 createdAt 和 updatedAt 字段
  },
)

/**
 * 定义 Model:
 * Model 是一个基于 Schema 的构造函数,用于创建和操作 MongoDB 中的文档。
 * 每个 Model 对应 MongoDB 中的一个集合(collection)。
 */
const User = model('User', userSchema)

module.exports = User

mongoose.model('User', userSchema)这里的User是什么东西?

  • 'User' 是一个字符串,指定了 MongoDB 数据库中集合的名称。
  • Mongoose 会根据这个名称在 MongoDB 中查找或创建对应的集合。通常,Mongoose 会将模型名称转换为小写并加上复数形式(如果适用)。例如,'User' 对应的集合名称通常是 users(MongoDB 集合名称通常是小写复数)。
  • 这个集合将存储所有基于 userSchema 定义的文档(documents)。每个文档代表一个用户,包含 username、password、email 等字段。

然后插入数据:

const { Router } = require('express')
const User = require('../models/user')

const router = Router()

// 添加用户
router.post('/users', async (req, res) => {
  try {
    const newUser = req.body
    // 使用 User 模型创建一个文档实例
    const user = User(newUser)
    // save函数返回一个 Promise。如果 save() 成功,Promise 会解析为保存的文档。
    const savedUser = await user.save(newUser)

    res.json({
      message: 'success',
      data: savedUser,
    })
  } catch (error) {
    res.status(400).json({
      message: 'failed',
      error: error,
    })
  }
})

module.exports = router

image.png

image.png

mongoose常用的内置函数

1️⃣ save() —— 保存文档实例

比如上面讲过的示例:

// 添加用户
router.post('/users', async (req, res) => {
  try {
    const newUser = req.body
    const user = User(newUser)
    // save函数返回一个 Promise。如果 save() 成功,Promise 会解析为保存的文档。
    const savedUser = await user.save(newUser)

    res.json({
      message: 'success',
      data: savedUser,
    })
  } catch (error) {
    res.status(400).json({
      message: 'failed',
      error: error,
    })
  }
})

要点

  • 只能在“文档实例”上调用,而不是模型本身。
  • 如果是新文档,会触发 insert;如果是已存在的,会执行 update
  • 会触发 Mongoose 中定义的中间件(middleware) (例如 pre('save')post('save'))。

2️⃣ create() —— 直接创建并保存

官方文档:mongoose.node.org.cn/docs/api/mo…

功能:一步完成“实例化 + 保存”。

// 创建用户
router.post('/users', async (req, res) => {
  try {
    // create() 相当于 new + save()
    const createdUser = await User.create(req.body)

    res.json({
      message: 'success',
      data: createdUser,
    })
  } catch (error) {
    res.status(400).json({
      message: 'failed',
      error: error,
    })
  }
})

要点

  • 推荐在直接创建新文档时使用。
  • 自动触发验证(validation)与中间件。
  • 支持批量创建:User.create([{}, {}, ...])

image.png

3️⃣ findById() —— 根据 _id 查询

官方文档:mongoose.node.org.cn/docs/api/mo…

比如请求url:http://localhost:3000/users/68f1f4e43ad19870bd3906b1

// 根据id获取用户
router.get('/users/:id', async (req, res) => {
  try {
    const user = await User.findById(req.params.id)
    // 如果用户不存在
    if (!user) return res.status(404).json({ message: '没找到这个用户' })

    res.json({ message: 'success', data: user })
  } catch (error) {
    res.status(400).json({
      message: 'failed',
      error: error,
    })
  }
})

要点

  • 自动将字符串 ID 转为 ObjectId
  • 如果 ID 无效,可能抛出 CastError

4️⃣ findOne() —— 查询单个文档

官方文档:mongoose.node.org.cn/docs/api/mo…

// 根据用户名查询用户
router.get('/users', async (req, res) => {
  try {
    const user = await User.findOne({ username: req.query.username })
    // 如果用户不存在
    if (!user) return res.status(404).json({ message: '没找到这个用户' })

    res.json({ message: 'success', data: user })
  } catch (error) {
    res.status(400).json({
      message: 'failed',
      error: error,
    })
  }
})

要点

  • 返回单个文档对象或 null
  • 如果你只需要一条结果,性能比 find() 更好。

5️⃣find() —— 查询多个文档

官方文档:mongoose.node.org.cn/docs/api/mo…

// 查询所有用户
router.get('/users', async (req, res) => {
  try {
    console.log(req)
    const users = await User.find()

    res.json({ message: 'success', data: users })
  } catch (error) {
    res.status(400).json({
      message: 'failed',
      error: error,
    })
  }
})

要点

  • 返回一个数组(即使只有一个文档)。
  • 支持条件:User.find({ age: { $gte: 18 } })
  • 可以链式调用:.sort(), .limit(), .select() 等。

6️⃣ findByIdAndUpdate() —— 查找并更新

官方文档:mongoose.node.org.cn/docs/api/mo…

// 查找并更新
router.put('/users/:id', async (req, res) => {
  try {
    console.log(req)
    const updatedUser = await User.findByIdAndUpdate(
      req.params.id,
      req.body,
      { new: true, runValidators: true } // 返回更新后的文档 + 启用验证
    )

    if (!updatedUser) return res.status(404).json({ message: 'User not found' })

    res.json({ message: 'success', data: updatedUser })
  } catch (error) {
    res.status(400).json({ message: 'failed', error: error.message })
  }
})

要点

  • { new: true } 表示返回更新后的文档。
  • { runValidators: true } 开启数据验证。
  • 不会触发 save() 中间件。

7️⃣ findByIdAndDelete() —— 查找并删除

官方文档:mongoose.node.org.cn/docs/api/mo…

// 删除用户
router.delete('/users/:id', async (req, res) => {
  try {
    const deletedUser = await User.findByIdAndDelete(req.params.id)

    if (!deletedUser) return res.status(404).json({ message: 'User not found' })

    res.json({ message: 'success', data: deletedUser })
  } catch (error) {
    res.status(400).json({ message: 'failed', error: error.message })
  }
})

要点

  • 返回被删除的文档(如果存在)。
  • 不会触发 remove()deleteOne() 的中间件。

8️⃣ countDocuments() —— 统计数量

官方文档:mongoose.node.org.cn/docs/api/mo…

router.get('/users/count', async (req, res) => {
  try {
    const count = await User.countDocuments({ age: { $gte: 18 } })
    res.json({ message: 'success', data: count })
  } catch (error) {
    res.status(500).json({ message: 'failed', error: error.message })
  }
})

要点

  • 常用于分页、统计。
  • estimatedDocumentCount() 更精确(但略慢)。

9️⃣ deleteMany() —— 批量删除

官方文档:mongoose.node.org.cn/docs/api/mo…

router.delete('/users', async (req, res) => {
  try {
    const result = await User.deleteMany({ active: false })
    res.json({ message: 'success', data: result })
  } catch (error) {
    res.status(500).json({ message: 'failed', error: error.message })
  }
})

要点

  • 返回 { acknowledged: true, deletedCount: X }
  • 不会触发 remove() 钩子。

1️⃣0️⃣ 分页查询

// ✅ 分页查询所有用户
router.get('/users', async (req, res) => {
  try {
    // 从查询参数中获取分页参数
    // 前端传 ?page=2&limit=5
    const page = parseInt(req.query.page) || 1        // 当前页码,默认为 1
    const limit = parseInt(req.query.limit) || 10     // 每页条数,默认为 10

    // 计算要跳过多少条数据
    // 比如页数是2时:(2-1)*10,跳过10个,就是上面一页中的数据
    const skip = (page - 1) * limit

    // 查询用户列表,使用 skip 和 limit 实现分页
    const users = await User.find()
      .skip(skip)     // 跳过前面几条
      .limit(limit)   // 只取本页的数量
      .sort({ createdAt: -1 }) // 可选:按创建时间倒序排序

    // 获取总数,用于计算总页数
    const total = await User.countDocuments()

    res.json({
      message: 'success',
      data: users,
      pagination: {
        total,              // 总记录数
        page,               // 当前页
        limit,              // 每页条数
        totalPages: Math.ceil(total / limit), // 总页数
      },
    })
  } catch (error) {
    res.status(400).json({
      message: 'failed',
      error,
    })
  }
})

条件分页示例:

const { page = 1, limit = 10, keyword = '' } = req.query
const filter = keyword ? { name: new RegExp(keyword, 'i') } : {}
const users = await User.find(filter)
  .skip((page - 1) * limit)
  .limit(limit)

时间戳

官方文档:mongoose.node.org.cn/docs/timest…

Mongoose 模式支持 timestamps 选项。如果设置 timestamps: true,Mongoose 会在你的模式中添加两个类型为 Date 的属性

  1. createdAt: 表示该文档创建时间的一个日期
  2. updatedAt: 表示该文档最后更新时间的一个日期

Mongoose 随后会在文档首次插入时设置 createdAt,并在你使用 save()updateOne()updateMany()findOneAndUpdate()update()replaceOne() 或 bulkWrite() 更新文档时更新 updatedAt

const userSchema = new Schema(
  {
    username: {
      type: String, // 注意,大写的String,不是typescript中的小写string
      required: true,
      unique: true,
    },
    password: {
      type: String,
      required: true,
    },
    email: String,
  },
  {
    timestamps: true, // 自动添加 createdAt 和 updatedAt 字段
  },
)

中间件

预处理中间件

官方文档:mongoose.nodejs.cn/docs/middle…

1. 密码加密示例

const userSchema = new Schema(
  {
    username: {
      type: String, // 注意,大写的String,不是typescript中的小写string
      required: true,
      unique: true,
    },
    password: {
      type: String,
      required: true,
    },
    email: String,
  },
  {
    timestamps: true, // 自动添加 createdAt 和 updatedAt 字段
  },
)

// 在执行save操作之前执行
userSchema.pre('save', async function (next) {
  // 如果修改的不是密码,就不用管
  if (!this.isModified('password')) return next()

  // 进行加密
  this.password = await bcrypt.hash(this.password, 12)
})

解释:

userSchema.pre('save', ...)
  • 这个函数会在 每次执行 user.save()User.create() 之前 自动运行。

userSchema.pre('save', async function (next) {
  • 注册一个 “save” 操作的前置中间件。
  • 注意这里用的是普通函数 function(),不是箭头函数,因为:
    • Mongoose 会把 this 绑定为当前文档实例
    • 如果用箭头函数,this 就不会指向文档了。

  if (!this.isModified('password')) return next()
  • this 表示当前要保存的文档对象(例如一个 User 实例)。
  • .isModified('password') 是 Mongoose 的内置方法,详情请看isModified。用来检测该字段是否被修改过。

实例方法

Models 的实例是documents。文档有很多自己的实例方法,比如我们前面用的save()。我们还可以定义自己的自定义文档实例方法。

比如:

// 验证密码
userSchema.methods.correctPassword = function (candidatePassword, userPassword) {
  return bcrypt.compare(candidatePassword, userPassword)
}

// 调用
const user = new User()
user.correctPassword(两个参数)

关联查询

多对多

一个文章article有多个标签tag,这个在mongoose中怎么设计呢?

我们可以使用标签独立集合 + 引用存储的设计方案。

思路:

  • Tag 单独作为一个集合存储(例如:tags)。
  • Article 文档中通过 ObjectId 数组 存储引用。

这样可以保证标签的可复用性、便于统计与管理。

Tag模型:

const mongoose = require('mongoose')

const tagSchema = new mongoose.Schema(
  {
    name: {
      type: String,
      unique: true,
    },
  },
  {
    timestamps: true,
  },
)

const Tag = mongoose.model('Tag', tagSchema)
module.exports = Tag

文章article:

const mongoose = require('mongoose')

const articleSchema = new mongoose.Schema(
  {
    title: {
      type: String,
      required: true,
    },
    summary: String,
    content: String,
    coverImage: String,
    tags: [
      {
        type: mongoose.Schema.Types.ObjectId,
        ref: 'Tag',
      },
    ],
  },
  { timestamps: true },
)

const Article = mongoose.model('Article', articleSchema)

module.exports = Article

这样设计的好处:

优点

  • 标签独立,方便复用、统计、筛选。
  • 可通过 populate 一键拿到标签详情。
  • 结构清晰,易维护。