MongoDB 简介

Mongoose 让操作更优雅

了解 ODM 是什么,学会用 Mongoose 的 Schema 和 Model 完成增删改查与数据校验。

🎯 引言

上一篇我们用官方驱动在 Node.js 中操作了 MongoDB,查询、写入都没问题。但你可能已经注意到:MongoDB 不限制文档结构,同一个集合里每条数据的字段可以各不相同。这种灵活在小项目里是优点,在项目变大后就容易变成混乱。

学完这篇文章,你能理解 ODM 是什么,会用 Mongoose 定义 Schema 和 Model,用更优雅的方式完成增删改查,还能让数据校验自动生效,把脏数据挡在数据库门外。


🧱 为什么需要 Mongoose

MongoDB 本身不会检查你写入的数据长什么样。比如往 user 集合插数据时,一次把字段写成 username,另一次手滑写成 usernmae,MongoDB 都会照单全收;年龄一会儿是数字、一会儿是字符串,它也完全不管。

学习阶段这不算什么,但项目大了以后,这些「写乱的文档」会让查询结果变得不可预期,排查起来非常头疼。

Mongoose 就是来解决这个问题的。它是一种 ODM(Object Document Mapping,对象文档映射)工具,可以给集合加上一层「结构约束 + 数据校验」:字段叫什么、是什么类型、是否必填、取值范围,都在代码里写清楚,不符合规则的数据直接拒绝写入。

可以打个比喻:官方驱动像一本自由生长的笔记本,想写什么就写什么,没人拦你;Mongoose 像一张带表格的登记表,每一栏填什么、怎么填都有规定,填错了当场被退回来。


🛠 安装与连接

本文基于 Node.js v22 LTSMongoose 9.x 编写和验证,MongoDB 还是我们之前用 Docker 跑起来的 mongo-demo 容器(mongo:8,端口 27017,账号 root、密码 123456)。

如果容器没在运行,先执行 docker start mongo-demo 把它启动起来。

新建一个项目目录,初始化后安装 mongoose:

mkdir node-mongoose && cd node-mongoose
npm init -y
npm install mongoose

然后新建 db.js,负责连接数据库:

db.js
const mongoose = require('mongoose');

async function connect() {
    await mongoose.connect(
        'mongodb://root:123456@localhost:27017/blog_demo?authSource=admin'
    );
    console.log('MongoDB 连接成功');
}

module.exports = connect;

连接串里有三个点要注意:

  • localhost:27017 是 Docker 容器映射出来的地址和端口。
  • /blog_demo直接写在连接串里的数据库名,连接成功后默认操作的就是它,不存在再创建。
  • ?authSource=admin 表示去 admin 数据库校验账号密码。因为 root 账号是在 admin 库里创建的,少了这个参数会认证失败,这是新手连接带密码的 MongoDB 时常见的坑。

✨ 定义 Schema 和 Model

Mongoose 的两个核心概念是 SchemaModel

  • Schema(模式):描述集合里文档的结构,相当于前面说的「登记表的表头」。
  • Model(模型):由 Schema 生成的操作入口,增删改查都通过它来完成。

我们为 user 集合定义一个 Schema:

user.js
const mongoose = require('mongoose');

const userSchema = new mongoose.Schema({
    username: { type: String, required: true },
    age: { type: Number, min: 0, max: 150 },
    city: { type: String, default: '未知' },
});

const User = mongoose.model('User', userSchema);

module.exports = User;

逐个字段来看:

  • username:字符串类型,required: true 表示必填,不写就报错。
  • age:数字类型,minmax 限制取值范围在 0 到 150 之间。
  • city:字符串类型,default: '未知' 表示不传时自动填「未知」。

mongoose.model('User', userSchema) 生成了 User 这个模型。注意首个参数 'User' 是模型名,Mongoose 会自动把它转成小写复数去对应集合,也就是操作 users 集合。这是它的约定,不用手动指定。

如果你前面几篇用的是 user 集合,注意 Mongoose 默认找的是 users。想让它继续用 user,可以显式传入第三个参数:mongoose.model('User', userSchema, 'user')

💡 用 Model 完成增删改查

有了 Model,增删改查都变得非常直白,方法名基本见名知意:

crud.js
const connect = require('./db');
const User = require('./user');

async function main() {
    await connect();

    // 新增
    const user = await User.create({ username: '小明', age: 20, city: '北京' });
    console.log('新增成功:', user);

    // 查询全部
    const users = await User.find();
    console.log('用户列表:', users);

    // 按 id 查询单个用户
    const found = await User.findById(user._id);
    console.log('按 id 查询:', found);

    // 更新
    await User.updateOne({ _id: user._id }, { age: 26 });

    // 删除
    await User.deleteOne({ _id: user._id });

    process.exit(0);
}

main();

和官方驱动对比着看:create 对应 insertOnefind 对应 findupdateOnedeleteOne 名字都一样。区别在于 create 会直接返回创建好的完整文档,不用再查一次;查询返回的也都是带 Schema 结构的文档对象,字段清清楚楚。

再来看数据校验怎么生效。试着创建一条缺少 username 的数据:

await User.create({ age: 20, city: '北京' });

运行后会抛出 ValidationError,错误信息里明确告诉你 username 是必填字段。同样,把 age 写成 200 也会因为超出 max: 150 被拒绝。脏数据根本进不了数据库,这就是 Schema 的价值。


⚖️ 和官方驱动怎么选

两个工具没有绝对的好坏,关键看场景:

  • 官方驱动:写法更接近 MongoDB 原生语法,包更小。写一次性脚本、数据迁移工具,或者需要精细控制查询细节时,用它更轻量。
  • Mongoose:自带结构定义和数据校验,还有中间件、关联查询等进阶能力。业务项目里多人协作、数据字段固定时,用它能省掉大量校验代码,结构也更清晰。

可以这么记:临时脚本用驱动,正经业务用 Mongoose。实际工作中业务项目里 Mongoose 是主流选择。


🧾 小节总结

  • MongoDB 不限制文档结构,灵活但容易写乱;Mongoose 是 ODM 工具,为集合加上结构约束和数据校验。
  • 安装:npm install mongoose,连接用 mongoose.connect(),连接串里直接带数据库名,带密码连接时要加 authSource=admin
  • Schema 描述文档结构(类型、必填、默认值、取值范围),Model 由 Schema 生成,是增删改查的操作入口。
  • 模型名 'User' 会自动对应小写复数的 users 集合,可用第三个参数显式指定集合名。
  • CRUD 用 User.create()User.find()User.findById()User.updateOne()User.deleteOne()
  • 不符合 Schema 规则的数据会抛出 ValidationError,无法写入数据库。

❓ 知识问答

Q1:ODM 和之前听过的 ORM 是什么关系?

思路是一样的,都是把代码里的对象映射到数据库。ORM 对应关系型数据库(对象映射到表和行),ODM 对应 MongoDB 这类文档数据库(对象映射到集合和文档)。

Q2:Schema 里没定义的字段还能写进去吗?

默认不能。Mongoose 会忽略 Schema 里没有声明的字段,写入时自动丢弃。这也是它帮你保持数据结构干净的一部分。

Q3:已有的集合里没有 _id 之外的约束,用 Mongoose 会报错吗?

不会。Schema 校验只发生在「通过 Mongoose 写入」时,已经存在的历史数据不受影响。查询时也能正常读出。

Q4:连接串里的 authSource=admin 能不能省略?

本课程的 Docker 环境不能省略,因为 root 账号是在 admin 库里创建的。如果你用的是专门为某个业务库创建的账号,authSource 就要填那个业务库的名字。

Q5:User.updateOne 更新时会触发 Schema 校验吗?

默认情况下 updateOne 不做完整校验(比如不会检查 required)。如果需要更新时也校验,可以传入 { runValidators: true } 选项。日常开发知道有这个开关即可。


🧪 小练习

练习一:给 userSchema 增加一个 email 字段,要求字符串类型、必填。然后创建一条带 email 的用户数据。

user.js
const userSchema = new mongoose.Schema({
    username: { type: String, required: true },
    age: { type: Number, min: 0, max: 150 },
    city: { type: String, default: '未知' },
    // 请在这里编写代码
});

练习二:写一个 findUsersByCity 函数,接收城市名参数,用 User.find() 返回该城市的所有用户。

crud.js
async function findUsersByCity(city) {
    // 请在这里编写代码
}

🎉 恭喜你已经掌握用 Mongoose 优雅操作 MongoDB 的技能啦!下一篇我们聊聊索引,看看数据量大起来之后,怎样给查询提速,让 MongoDB 保持轻快。