☰
mongoose 入门(一)mongoose 实现数据的增、删、改、查、默认参数、模块化
2026/10/7 14:22:56 网站建设 项目流程

1. 从一次「数据写不进去」说起:mongoose 增删改查到底解决什么问题

如果你刚开始写 Node.js 后端,大概率会遇到这样的场景:接口写好了,前端也调通了,但数据就是没落库。打开 MongoDB 客户端一看,集合是空的,控制台也没报错。这种「静默失败」在原生 mongodb 驱动里很常见,因为回调里的 err 被忽略了,或者集合名对不上。

mongoose 就是来解决这类问题的。它是 Node.js 环境下对 MongoDB 的对象模型工具(ODM),核心价值有三个:用 Schema 把「表结构」显式定义出来,字段类型、默认值、必填校验都能写清楚;用 Model 封装增删改查,不用手写一堆 collection 操作;用连接管理把数据库连接和业务代码解耦。简单说,它让你用接近关系型数据库的思维去操作非关系型数据库。

这篇面向的是本地 MongoDB + Node.js 初学者,假设你已经装好了 MongoDB(默认端口 27017),会用 npm,能跑一个node app.js。我会从零搭一个数据层,交付四样东西:可复制的 Schema 定义、默认参数写法、完整 CRUD 示例、模块化目录拆分。每一步都有验证方法,你跟着敲完就能看到真实结果。

热词里的「增删改查」「默认参数」「模块化」是三个递进层次:先能读写,再能省事,最后能维护。很多人卡在第二步——每次新增都要手动传 status,忘了就存成 undefined,查询时又对不上。默认参数就是治这个的。

先明确一个概念区分,后面会反复用到:

概念作用能否操作数据库
Schema定义字段结构和类型不能
Model由 Schema 生成,操作集合能
实例(document)Model 的 new 出来的对象能,save 后落库

Schema 只是「图纸」,Model 才是「施工队」。你定义mongoose.Schema({...})时数据库毫无感知,只有mongoose.model('User', UserSchema)之后,才有能力去操作users集合。这个区分不清楚,后面模块化时很容易把 Schema 和 Model 混着导出,导致model is not a function之类的报错。

环境准备只需要一条命令:

npm i mongoose --save

装完确认版本,mongoose 8.x 和 6.x 在连接选项上有差异,后面排障会用到:

npm ls mongoose

到这里,问题场景和工具定位就清楚了。接下来先把连接和模型建起来,这是所有增删改查的地基。

2. 前置准备:mongoose 连接本地 MongoDB 与 Schema 定义

这一节的目标是让数据库连接成功、模型能创建。很多人跳过连接验证直接写 CRUD,结果报错时不知道是连接问题还是模型问题,排查成本翻倍。

先看连接。最简写法是mongoose.connect('mongodb://127.0.0.1:27017/eggcms'),但生产习惯上建议带上选项和回调,方便确认状态:

const mongoose = require('mongoose'); mongoose.connect('mongodb://127.0.0.1:27017/eggcms', { useNewUrlParser: true }, function (err) { if (err) { console.log('连接失败:', err); return; } console.log('数据库连接成功'); });

这里有个细节:useNewUrlParser在 mongoose 6 之后已经默认开启,写不写都行,但老教程里常见,保留不影响。真正要留意的是连接是异步的,connect返回的是 Promise,回调触发时连接才真正建立。如果你在连接成功前就执行查询,mongoose 会帮你缓冲(buffering),但缓冲超时会报Operation buffering timed out,这是新手最常见的坑之一。

连接串的格式拆解一下,方便你对照自己的环境:

mongodb://用户名:密码@主机:端口/数据库名 mongodb://127.0.0.1:27017/eggcms

本地无密码就省略用户名密码部分。数据库名eggcms不存在也没关系,MongoDB 在第一次写入时会自动创建。

连接通了,定义 Schema。Schema 的字段类型要和实际数据对应,写错了不会立刻报错,但查询时会返回空或类型异常:

const UserSchema = mongoose.Schema({ name: String, age: Number, status: { type: Number, default: 1 } });

注意status的写法,这是默认参数的雏形。type指定类型,default指定不传时的值。对比name: String这种简写,对象写法才能挂默认值、必填、校验等配置。

然后是 Model。mongoose.model有两个参数和三个参数两种用法,区别在集合名:

// 两个参数:模型名 User 会映射到复数集合 users const User = mongoose.model('User', UserSchema); // 三个参数:显式指定集合名 user const User = mongoose.model('User', UserSchema, 'user');

规则是:两个参数时,mongoose 把模型名转小写并加 s,User→users;三个参数时,第三个参数就是集合名,原样使用。如果你数据库里已经有user集合(单数),就必须用三个参数的写法,否则会去操作一个空的users集合,查不到数据还以为代码错了。

模型名首字母必须大写,这是 mongoose 的约定,小写虽然不报错但容易和实例变量混淆。

验证连接和模型是否就绪,跑一个最小脚本:

const mongoose = require('mongoose'); mongoose.connect('mongodb://127.0.0.1:27017/eggcms', {}, function (err) { if (err) { console.log(err); return; } console.log('数据库连接成功'); }); const UserSchema = mongoose.Schema({ name: String, age: Number, status: { type: Number, default: 1 } }); const User = mongoose.model('User', UserSchema, 'user'); User.find({}, function (err, docs) { if (err) { console.log(err); return; } console.log('查询结果:', docs); });

执行node app.js,看到「数据库连接成功」和「查询结果: []」就说明地基没问题。空数组是正常的,因为还没写入数据。如果这里就报错,先解决连接问题,别往下走。

一个容易忽略的点:mongoose.connect的第二个参数如果传空对象{},在 mongoose 8 里是合法的;但如果你从老项目复制代码,可能看到useUnifiedTopology之类的选项,新版本已废弃,传了会有警告但不影响运行。

连接和模型都验证通过后,就可以进入真正的增删改查了。下一节把四个操作写成可复制的代码,每个都带结果说明。

3. 可复制配置:mongoose 增删改查完整代码与默认参数写法

这一节是核心,把增、删、改、查四个操作写成能直接跑的代码。我按「先查、再增、后改、最后删」的顺序排,因为新增后通常要查一下确认,改删也需要先有数据。

先给一份完整的app.js,包含连接、Schema、Model 和四个操作。你可以整段复制,改一下数据库名就能跑:

const mongoose = require('mongoose'); mongoose.connect('mongodb://127.0.0.1:27017/eggcms', { useNewUrlParser: true }, function (err) { if (err) { console.log('连接失败:', err); return; } console.log('数据库连接成功'); }); const UserSchema = mongoose.Schema({ name: String, age: Number, status: { type: Number, default: 1 } }); const User = mongoose.model('User', UserSchema, 'user'); // 1. 增加数据 const user = new User({ name: '张三', age: 30 // status 不传,走默认值 1 }); user.save(function (err, doc) { if (err) { console.log('新增失败:', err); return; } console.log('新增成功:', doc); // 2. 查询数据 User.find({}, function (err, docs) { if (err) { console.log('查询失败:', err); return; } console.log('查询结果:', docs); // 3. 修改数据 User.updateOne({ name: '张三' }, { name: '张三丰' }, function (err, res) { if (err) { console.log('修改失败:', err); return; } console.log('修改结果:', res); // 4. 删除数据 User.deleteOne({ name: '张三丰' }, function (err, result) { if (err) { console.log('删除失败:', err); return; } console.log('删除结果:', result); }); }); }); });

这段代码嵌套比较深,是为了让你一次跑完看到全流程。实际项目里会用 async/await 拆开,后面模块化部分会给。

逐个拆解关键点。

新增用new Model()实例化,再调save()。注意status没传,但保存后doc.status会是 1,这就是默认参数生效。save的回调第二个参数是保存后的文档,包含自动生成的_id。

查询用User.find({}),空对象表示查全部。返回的是数组。如果只想查一条,用findOne。查询条件支持各种操作符,比如{ age: { $gt: 20 } }查年龄大于 20 的。

修改用updateOne(条件, 更新内容, 回调)。第一个参数是筛选条件,第二个是要改的字段。回调的res包含matchedCount和modifiedCount,能看出匹配了几条、改了几条。注意updateOne只改第一条匹配的,要改多条用updateMany。

删除用deleteOne(条件, 回调),回调的result里有deletedCount。同样,删多条用deleteMany。

默认参数的写法值得单独说。除了default,Schema 还支持这些配置:

const UserSchema = mongoose.Schema({ name: { type: String, required: true }, age: { type: Number, min: 0, max: 150 }, status: { type: Number, default: 1, enum: [0, 1, 2] }, createdAt: { type: Date, default: Date.now } });

required必填,不传会报校验错误;min/max数值范围;enum枚举限定;default: Date.now是函数,每次新增取当前时间。这些配置让数据层自带约束,比在业务代码里到处写 if 判断干净得多。

一个实测经验:default只在字段为 undefined 时生效。如果你传了status: null,默认值不会覆盖,会存成 null。所以前端传参时要过滤掉 null,或者用set转换。

跑完上面的脚本,控制台应该依次输出:

数据库连接成功 新增成功: { _id: ..., name: '张三', age: 30, status: 1, __v: 0 } 查询结果: [ { _id: ..., name: '张三', age: 30, status: 1, __v: 0 } ] 修改结果: { acknowledged: true, modifiedCount: 1, ... } 删除结果: { acknowledged: true, deletedCount: 1 }

看到status: 1就说明默认参数生效了。__v是 mongoose 的版本字段,用于并发控制,不用管它。

到这里,单文件的增删改查就完成了。但所有代码堆在一个文件里,项目一大就难维护。下一节做模块化拆分。

4. 模块化拆分:mongoose 数据层目录结构与验证请求

单文件能跑通,但真实项目里连接、模型、业务逻辑要分开。这一节把上面的代码拆成model/db.js、model/user.js、app.js三个文件,这是 Node.js 后端最常见的分层方式。

目录结构:

project/ ├── model/ │ ├── db.js │ └── user.js └── app.js

model/db.js只负责连接,导出 mongoose 实例:

const mongoose = require('mongoose'); mongoose.connect('mongodb://127.0.0.1:27017/eggcms', { useNewUrlParser: true }, function (err) { if (err) { console.log('连接失败:', err); return; } console.log('数据库连接成功'); }); module.exports = mongoose;

model/user.js引入 db.js,定义 Schema 和 Model,导出 Model:

const mongoose = require('./db.js'); const UserSchema = mongoose.Schema({ name: String, age: Number, status: { type: Number, default: 1 } }); module.exports = mongoose.model('User', UserSchema, 'user');

app.js只写业务逻辑,引入 Model 直接用:

const UserModel = require('./model/user.js'); async function main() { try { const user = new UserModel({ name: '李四', age: 40 }); const saved = await user.save(); console.log('新增成功:', saved); const docs = await UserModel.find({}); console.log('查询结果:', docs); const updated = await UserModel.updateOne( { name: '李四' }, { name: '李四改' } ); console.log('修改结果:', updated); const deleted = await UserModel.deleteOne({ name: '李四改' }); console.log('删除结果:', deleted); } catch (err) { console.log('操作失败:', err); } } main();

这里把回调改成了 async/await,代码更线性,错误统一用 try/catch 捕获。mongoose 的方法都返回 Promise,所以能直接 await。

模块化的关键点是连接只执行一次。db.js被require时,Node.js 会缓存模块,所以即使多个模型文件都引入它,mongoose.connect也只跑一次。如果你在每个模型文件里都写 connect,会报Trying to open a connection that is already open之类的警告。

验证模块化是否成功,跑node app.js,输出应该和单文件版一致。如果报Cannot find module './db.js',检查路径和文件名大小写,Linux 环境下大小写敏感。

再给一个更贴近真实接口的验证方式,用 Express 起一个简单服务:

const express = require('express'); const UserModel = require('./model/user.js'); const app = express(); app.use(express.json()); app.post('/users', async (req, res) => { try { const user = new UserModel(req.body); const saved = await user.save(); res.json({ code: 0, data: saved }); } catch (err) { res.json({ code: 1, msg: err.message }); } }); app.get('/users', async (req, res) => { try { const docs = await UserModel.find({}); res.json({ code: 0, data: docs }); } catch (err) { res.json({ code: 1, msg: err.message }); } }); app.listen(3000, () => { console.log('服务启动:http://127.0.0.1:3000'); });

用 curl 验证:

curl -X POST http://127.0.0.1:3000/users \ -H "Content-Type: application/json" \ -d '{"name":"王五","age":25}' curl http://127.0.0.1:3000/users

第一条返回新增的文档,status自动为 1;第二条返回包含王五的数组。看到这个结果,说明模块化数据层已经跑通。

模块化之后,新增字段只需要改user.js的 Schema,业务代码不用动。这就是分层的价值。下一节处理常见报错。

5. 本篇常见错排查:mongoose 连接失败与 CRUD 报错对照

这一节按真实报错信息来,你遇到哪个查哪个。我把最常见的几类整理成对照表,再逐个说明。

报错信息原因解决
MongooseServerSelectionError: connect ECONNREFUSEDMongoDB 没启动或端口不对启动 mongod,确认 27017
Operation buffering timed out after 10000ms连接未建立就执行查询等连接回调后再操作
Cannot overwrite model once compiled同一模型名重复定义用mongoose.models.User || mongoose.model(...)
ValidationError: name: Path name is required必填字段没传补字段或去掉 required
CastError: Cast to Number failed类型不匹配检查传参类型
E11000 duplicate key error唯一索引冲突检查 unique 字段

连接被拒最常见。先确认 MongoDB 在跑:

# macOS/Linux ps aux | grep mongod # 或者直接连一下 mongosh mongodb://127.0.0.1:27017

连不上就启动服务。Windows 用服务管理器,macOS 用brew services start mongodb-community。

buffering timed out是连接没就绪就查询。mongoose 默认缓冲 10 秒,超时抛这个错。解决方法是把操作放在连接回调里,或者用await mongoose.connect(...)确保连接完成:

await mongoose.connect('mongodb://127.0.0.1:27017/eggcms'); // 连接完成后再执行查询 const docs = await User.find({});

Cannot overwrite model出现在热重载或重复 require 时。因为mongoose.model('User', ...)第二次调用会冲突。标准写法:

module.exports = mongoose.models.User || mongoose.model('User', UserSchema, 'user');

这样已存在就复用,不存在才创建。

ValidationError是 Schema 校验没通过。报错信息里会指明哪个字段,比如Path name is required。要么补上字段,要么把required: true去掉。注意required对空字符串也生效,如果允许空串,要写required: function() { return this.name !== ''; }。

CastError是类型转换失败。比如 Schema 定义age: Number,你传了age: 'abc',mongoose 转不了就报错。检查前端传参,或者在 Schema 里用set做转换。

E11000是唯一索引冲突。如果你给某字段加了unique: true,重复插入会报这个。注意unique不是校验器,是索引,第一次插入重复值时可能不报错,需要等索引建好。

还有一个隐蔽的坑:集合名对不上。Schema 和 Model 都对了,但查询返回空数组。用mongosh看一下实际集合名:

mongosh use eggcms show collections

如果集合叫user而你用两个参数创建 Model,mongoose 会去找users,自然查不到。改成三个参数mongoose.model('User', UserSchema, 'user')即可。

最后提醒一个连接字符串的坑:localhost在某些系统上解析到 IPv6 的::1,而 MongoDB 只监听 IPv4,导致连接失败。把localhost换成127.0.0.1通常能解决。

排障的核心思路是:先确认连接,再确认集合名,最后看字段类型和校验。按这个顺序,大部分问题都能定位。

6. 从本地到稳定调用:mongoose 数据层的下一步

本地跑通增删改查和模块化之后,下一步通常是接入更规范的服务。如果你在写 Claude Code 相关的工具链,或者需要把模型调用能力接进 Node.js 后端,数据层和调用层可以分开管理。模型调用这块,TaoToken 提供了兼容的接口,配置方式和 mongoose 连接类似,都是先建连接再操作。

模型对话调试可以用 TaoToken 模型对话,先把请求跑通再写进代码。长期做编码和 Agent 的话,Coding Plan 更适合持续调用。API Key 在 API Keys 页面管理,接入细节看 接入文档,接口地址是https://taotoken.net/api。

回到 mongoose 本身,几个可以继续深入的方向:用populate做关联查询,用aggregate做聚合统计,用mongoose.Schema.Types.ObjectId建外键。这些都是在今天这套模块化结构上扩展,Schema 加字段、Model 加方法即可。

最后给一个实用技巧:开发阶段打开 mongoose 的调试日志,能看到实际执行的 MongoDB 命令,排查查询问题时非常有用:

mongoose.set('debug', true);

加在db.js的 connect 之前,控制台会打印每条操作对应的底层命令。上线前记得关掉,否则日志量很大。

数据层搭好之后,增删改查就是日常操作了。把 Schema 当契约维护,字段变更走版本管理,比事后补数据省事得多。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询