Skip to content

使用 Node 操作数据库 - Mongoose与附录(Day12 3/3) ​

本页对应 Day12_Node&MongoDB,共 7 段视频,约 1 小时 12 分。

学习目标 ​

  • 能够使用 Mongoose 连接 MongoDB,并监听连接成功与失败事件,说明连接代码应放在应用启动流程中的位置。
  • 能够为集合定义 Schema、创建并导出 Model,并在其他模块中正确导入和使用该 Model。
  • 能够使用 Mongoose Model 完成单条/批量新增、单条/多条删除、单条/多条更新,并验证数据库结果。
  • 能够使用 Mongoose 完成单条查询、多条查询、全量查询,并应用字段筛选、排序、skip 与 limit。
  • 能够在记账本项目中集成 MongoDB,在路由回调中使用 Model 实现账单新增、列表查询和按 _id 删除,并渲染成功或失败页面。

知识点 ​

单条与批量新增:先明确连接生命周期 ​

为什么需要:Mongoose 是 Node 操作 MongoDB 的常用 ODM,新增数据是 CRUD 的入口。转写稿先演示 insertOne,再演示 insertMany,是为了区分“一个文档”和“多个文档”的参数形式。

怎么想:单条新增第一个参数是对象;批量新增第一个参数是数组,数组里放多个对象。实际开发中数据库连接要长期保持,不能每次操作完就关闭;演示中调用 mongoose.connection.close() 只是验证脚本能退出。

怎么做:

js
const mongoose = require('mongoose');
mongoose.connect('mongodb://127.0.0.1:27017/project04');

const songSchema = new mongoose.Schema({
  author: String,
  language: String,
  duration: Number,
  hot: Number,
  title: String
});
const Song = mongoose.model('Song', songSchema, 'song');

// 单条
Song.insertOne({ title: '示例', author: '某某', hot: 100 });

// 批量:require JSON 会自动转成对象
Song.insertMany(require('./data.json').song_list);

易错点:require('./data.json') 得到的是对象,要取对象里的数组属性;Schema 只声明 5 个字段时,源数据多余字段不会插入;不要传空数组冒充批量数据。转写稿中运行 02 报“user model 未定义”,原因就是复制代码后模型名没改,改回歌曲模型即可。

删除:deleteOne 与 deleteMany 的区别 ​

为什么需要:删除要能精确删一条,也能按条件删多条。转写稿先试 delete 报错,才改为 deleteOne,这是方法名记忆点。

怎么想:deleteOne(条件, 回调) 只删除满足条件的第一个文档;deleteMany(条件, 回调) 删除所有满足条件的文档;deleteMany({}) 会清空集合。

怎么做:

js
Song.deleteOne({ author: '林菲如' }).then(res => {
  console.log(res.deletedCount); // 删除数量
});

Song.deleteMany({ author: '林菲如' });
// 清空集合
Song.deleteMany({});

易错点:返回的是删除结果,不是被删文档;deleteOne 不是删所有;清空后可用 02 批量新增脚本恢复数据,说明“程序化造数据”比手工恢复可靠。

更新:updateOne 与 updateMany 的局部更新 ​

为什么需要:业务里经常只改某个字段,而不是整份文档替换。转写稿用作者改名演示局部更新。

怎么想:updateOne 改满足条件的第一条,updateMany 改所有满足条件的文档。Mongoose 有 Schema 约束,通常只更新指定字段,不会随意把整条文档覆盖成缺字段状态。

怎么做:

js
Song.updateOne(
  { author: '蔡徐坤' },
  { $set: { author: '高孝乐' } }
).then(res => console.log(res));

Song.updateMany(
  { author: '蔡徐坤' },
  { $set: { author: '林菲如' } }
);

易错点:条件不写会命中第一条;要批量改必须用 updateMany;转写稿里“高考证/高考乐/高小乐”是同音转写误差,代码里应保持同一个目标值,避免条件与更新值写混。

查询:findOne、findById、find 与链式操作 ​

为什么需要:查询是最常用操作,还要支持按条件、按 ID、字段筛选、排序和截取。

怎么想:查一条用 findOne 或 findById;查多条用 find;find() 不写条件就是全查。字段筛选用 select,排序用 sort,截取用 skip 和 limit,最后用 exec 执行回调。

怎么做:

js
Song.findOne({ author: '高孝乐' }).then(data => console.log(data));
Song.findById('文档_id字符串').then(data => console.log(data));

Song.find({ hot: { $gt: 300 } })
  .select({ author: 1, title: 1, _id: 0 })
  .sort({ hot: 1 })       // 1 升序,-1 降序
  .skip(90)
  .limit(10)
  .exec((err, data) => console.log(data));

易错点:_id 默认会带出,不想显示要显式设 _id: 0;skip 从 0 开始,转写稿中“从第 90 个开始看 10 个”出现条数差一,就是索引从 0 起;链式查询别忘了 exec。

记账本项目接入:连接一次,模型独立,路由做 CRUD ​

为什么需要:项目启动后要长期服务,不能每个请求都连数据库。转写稿的分析是:连接放在启动文件 www,连接成功后再启动 HTTP 服务;模型放到 models,谁需要谁导入。

怎么想:整个项目连同一个数据库一次即可;一个集合对应一个 Schema 和一个 Model;增删改查写在路由回调里。

怎么做:

js
// www:连接成功后再启动服务
mongoose.connect('mongodb://127.0.0.1:27017/account_project');
mongoose.connection.on('error', err => console.log('数据库连接失败', err));
mongoose.connection.once('open', () => {
  server.listen(3000);
});

// models/account.js
const mongoose = require('mongoose');
const accountSchema = new mongoose.Schema({
  title: String,
  time: String,
  type: Number,
  account: Number
});
module.exports = mongoose.model('Account', accountSchema, 'accounts');

// routes/account.js
const Account = require('../models/account');
Account.create(req.body, err => { /* 渲染成功/失败 */ });
Account.find((err, data) => { /* 渲染列表 */ });
Account.deleteOne({ _id: req.query.id }, err => { /* 删除 */ });

易错点:每个模块都要单独 require('mongoose');删除时模板里要传 _id,不是 id;不要在每个路由请求里 connect;转写稿还提到“假删”思路——用 isDelete 标记而不真删,查询时只查未删除数据。

本节附录把 mongoose 的增删改查与记账本项目升级串起来。下面按“为什么需要→怎么想→怎么做→易错点”整理,代码按转写稿示意整理。

连接与关闭:理解 mongoose 生命周期 ​

为什么需要:执行完脚本后进程不退出,容易误以为程序卡住。怎么想:mongoose.connection.close() 能关闭连接;但实际后端服务要常驻,不能每次请求都连一次、断一次。怎么做:学习脚本可在确认写入成功后关闭;项目服务只连一次。易错点:生产服务不要随手关连接;要监听 error,连接失败时不要继续启动 HTTP 服务。

js
const mongoose = require('mongoose');
mongoose.connect('mongodb://127.0.0.1:27017/project04');
// 学习脚本可关闭:mongoose.connection.close();

批量插入:数组与 JSON 导入 ​

为什么需要:一条条 create 效率低。怎么想:insertMany 接收对象数组;Schema 只保留声明字段,多余字段不会入库。怎么做:用 require 导入 JSON,JSON 会自动转成对象,取出数组交给 insertMany。易错点:运行 node 02 报 userModel is not defined,是模型名忘改;空数组不是“全部”,不要误写;JSON 不需要再手动 JSON.parse。

js
const SongSchema = new mongoose.Schema({
  title: String, author: String, language: String,
  duration: Number, hot: Number
});
const SongModel = mongoose.model('songs', SongSchema);
SongModel.insertMany(list, (err, data) => { /* ... */ });

删除:deleteOne 与 deleteMany ​

为什么需要:按条件删一条或删一批。怎么想:deleteOne 只删第一个匹配文档,deleteMany 删所有匹配;空对象 {} 会清空集合。怎么做:条件写成对象,回调里看 err,结果里有 deletedCount。易错点:没有 delete 方法,报错后应改 deleteOne/deleteMany;清空后可用插入脚本恢复数据。

js
SongModel.deleteOne({ author: '某作者' }, (err, res) => {});
SongModel.deleteMany({}, (err, res) => {});

更新:updateOne/updateMany 不整体覆盖 ​

为什么需要:只改部分字段,避免用新文档覆盖旧文档。怎么想:Schema 已约束字段,更新只改给定字段;updateOne 改第一个匹配,updateMany 改所有匹配。怎么做:条件对象 + 更新对象。易错点:条件写成空对象会全改;不要以为会整体替换旧文档。

js
SongModel.updateOne({ author: '某作者' }, { author: '新作者' }, (err, res) => {});
SongModel.updateMany({ author: '某作者' }, { author: '新作者' }, (err, res) => {});

查询:findOne、findById、find 与链式操作 ​

为什么需要:单条、按 ID、多条、字段筛选、排序分页都常用。怎么想:findOne 返回对象,find 返回数组;select 指定字段,_id 默认带出,设 _id:0 可排除;sort 的 1 升序、-1 降序;skip 从 0 开始。怎么做:find(...).select(...).sort(...).skip(...).limit(...).exec(cb)。易错点:skip(90).limit(10) 实际是第 91—100 条;字段名拼错会查不到;回调参数顺序是 err, data。

js
SongModel.find({ hot: { $gt: 300 } })
  .select({ author: 1, title: 1, _id: 0 })
  .sort({ hot: 1 })
  .skip(90).limit(10)
  .exec((err, data) => {});

项目升级:先连库,再启动 HTTP ​

为什么需要:数据库连不上时启动服务没有意义。怎么想:整个项目只连一次,所有路由共享连接;连接成功后回调里再 app.listen。怎么做:在 www 中导入 mongoose、建立连接、监听 open 和 error,把启动 HTTP 的代码放进成功回调。易错点:不要在路由回调里连接数据库,否则每次请求都新建连接;连接失败要提示并停止启动。

js
mongoose.connect('mongodb://127.0.0.1:27017/account-project');
mongoose.connection.on('error', err => { /* 提示失败 */ });
mongoose.connection.on('open', () => { app.listen(3000); });

Schema 与 Model:models 目录暴露模型 ​

为什么需要:一个集合对应一个 Schema/Model,路由需要模型做 CRUD。怎么想:models/account.js 专门创建 Schema 和 Model,并 module.exports 暴露 Model;谁需要谁 require。怎么做:每个模块都要自己导入 mongoose。易错点:集合名与模型名要对应;不要把连接代码写进模型文件。

js
const mongoose = require('mongoose');
const AccountSchema = new mongoose.Schema({
  title: String, time: String, type: Number,
  account: Number, remark: String
});
module.exports = mongoose.model('accounts', AccountSchema);

路由 CRUD:create、find、deleteOne ​

为什么需要:把数据库操作放进路由回调,页面才能读写数据。怎么想:添加用 create,首页用 find,删除用 deleteOne({_id: req.query.id})。怎么做:模板里写 item._id,不是 item.id。易错点:删除/添加失败要渲染失败页;真实项目常做“假删”,用 isDelete 标记而不真删,便于恢复数据。

js
AccountModel.create(req.query, err => { /* 成功或失败页面 */ });
AccountModel.find((err, data) => { res.render('index', { data }); });
AccountModel.deleteOne({ _id: req.query.id }, err => { /* ... */ });

第一步:连接成功后再启动 HTTP 服务 ​

为什么需要:记账本项目要把账单存进 MongoDB,就必须先建立数据库连接。转写稿里反复强调:连接数据库的代码不能写在路由回调里,否则用户每请求一次就可能创建一个连接,容易把连接“连坏”。连接应该在项目启动时做一次。

怎么想:项目启动入口是 bin/www,HTTP 服务也是在这里启动的。因此应先导入 mongoose、建立连接、监听连接成功和失败事件;只有连接成功,才启动后面的 HTTP 服务。数据库都连不上,启动服务没有意义。

怎么做:在 bin/www 中完成前三步:

js
const mongoose = require('mongoose');

mongoose.connect('mongodb://127.0.0.1:27017/account-project');

mongoose.connection.on('error', err => {
  console.log('数据库连接失败,应用无法启动,请排查错误', err);
});

mongoose.connection.once('open', () => {
  // 数据库连接成功后,再启动 HTTP 服务
});

原笔记步骤要保留:

第一步:
   当数据库连接成功之后,才能启动 http 服务
   在 bin/www 里面:
   	1. 导入模块
    2. 建立连接
    3. 监听连接成功事件  连接失败事件

易错点:不要把连接代码写进 routes/account.js;不要连接失败还继续启动服务;调试脚本里可以关闭连接,但实际后端服务运行期间不需要关闭连接。

第二步:每个集合一个模型文件 ​

为什么需要:Mongoose 需要 schema 规定文档结构,再用 model 对应集合。记账项目后面可能既有账单集合,又有用户集合,所以每个集合应单独建模型文件,谁需要谁导入。

怎么想:models/account.js 只负责创建账单的 schema 和 model,并把模型对象作为暴露数据。转写稿里说得很清楚:需要几个集合,就建几个模型文件。

怎么做:创建 models/account.js,保留原笔记步骤:

第二步
	创建 models/account.js 文件
		4. 创建 schema
		5. 创建 model
	并将Model对象作为暴露数据

关键代码思路:

js
const mongoose = require('mongoose');

const accountSchema = new mongoose.Schema({
  title: String,
  time: String,
  type: Number,
  account: Number
});

module.exports = mongoose.model('accounts', accountSchema);

易错点:模型文件里也要各自导入 mongoose;暴露的是 model 对象,不是 schema;集合名和模型名要对应好。转写稿调试时出现“user model 没有定义”,就是因为变量名忘了改。

第三步:在路由里导入模型做增删改查 ​

为什么需要:路由负责接收请求并返回页面或数据,具体数据库操作应交给模型完成。这样连接、模型、路由职责分开。

怎么想:在 routes/account.js 导入 models/account.js 得到的 model 对象,然后调用 create、deleteOne、deleteMany、updateOne、updateMany、find、findOne、findById 等方法。原笔记步骤保留:

第三步
	在 rountes/account.js 中
	导入  models/account.js  得到 model 对象
	使用 model 对象进行增删改查

怎么做:添加用 create(req.body, ...);列表用 find((err, data) => ...);删除用 deleteOne({_id: req.query.id}, ...);更新用 updateOne(条件, 更新对象, ...)。查询单条可用 findOne 或 findById;字段筛选用 select({ title: 1, author: 1, _id: 0 }) 后接 exec;排序用 sort({ hot: 1 });截取用 skip(0)、limit(10)。

易错点:没有 delete 方法,只有 deleteOne/deleteMany;deleteOne 只删满足条件的第一条;_id 是下划线开头,模板里写 item._id;find 返回数组,findOne 返回对象;select 中 _id 默认会带出,除非设 _id: 0;skip 从 0 开始计数。转写稿里批量插入用数组,JSON 文件可通过 require 导入并自动转成对象,这也是调试时要记住的规则。

课堂画面 ​

课堂画面 1

课堂画面 2

课堂画面 3

课堂画面 4

课堂画面 5

课堂画面 6

代码示例 ​

1. 记账本项目:连接数据库 + 创建 Schema 和 Model ​

js
// ===== bin/www =====
// 只在启动时连接一次数据库;连接成功后才启动 HTTP 服务
const mongoose = require('mongoose');

// 第二步:建立连接(库不存在会自动创建)
mongoose.connect('mongodb://127.0.0.1:27017/account-project');

// 第三步:监听连接失败事件
mongoose.connection.on('error', function (err) {
  console.log('数据库连接失败,整个应用无法启动,请排查错误', err);
});

// 第三步:监听连接成功事件
mongoose.connection.once('open', function () {
  // 以下为原来 bin/www 里启动服务的代码,整体挪进成功回调里
  const port = normalizePort(process.env.PORT || '3000');
  app.set('port', port);

  const server = http.createServer(app);
  server.listen(port);
  server.on('error', onError);
  server.on('listening', onListening);
});
js
// ===== models/account.js =====
// 一个集合对应一个文件;这个模块只负责“建 Schema + 建 Model 并暴露”
const mongoose = require('mongoose');   // 每个模块有自己的作用域,都要单独导入

// 第四步:创建 Schema(规定文档里有哪些字段、什么类型)
const accountSchema = new mongoose.Schema({
  title: String,     // 事项
  time: String,      // 时间(前端用 moment 处理成字符串)
  type: Number,      // 类型
  account: Number    // 金额
});

// 第五步:创建 Model,第一个参数是集合名,第二个是 Schema
// 把 Model 对象作为暴露数据,谁需要增删改查谁就 require 它
module.exports = mongoose.model('accounts', accountSchema);

要点与易错点

  • 连接只需一次,必须写在 bin/www(启动文件)里,不能写在路由回调里,否则每次请求都会新建一个连接。
  • 顺序是“先连库、再启服务”:把原来的 server.listen(...) 整段剪切到 connection.once('open') 的回调中;连不上库就没必要启动 HTTP 服务。
  • 监听事件用 mongoose.connection.on('error') 和 mongoose.connection.once('open'),注意 connection 是小写属性。
  • models/account.js 里要单独 require('mongoose'),不能指望 bin/www 里导入过就能用。
  • mongoose.model('accounts', schema) 的集合名建议用复数(对应数据库里的 accounts);Model 暴露出去后,别处拿到的是 Model 对象而不是 Schema。
  • 安装依赖时用 mongoose@6(npm i mongoose@6),版本不同 API 有差异。

2. Mongoose 增删改查完整演示脚本 ​

js
// 运行:node 02_crud.js
const mongoose = require('mongoose');

mongoose.connect('mongodb://127.0.0.1:27017/project04');

const songSchema = new mongoose.Schema({
  author: String,
  language: String,
  hot: Number,
  title: String
});
const SongModel = mongoose.model('songs', songSchema);

mongoose.connection.once('open', async () => {

  // ===== 一、批量插入 =====
  const data = require('./data.json');        // json 当模块导入,得到的是「对象」
  let res = await SongModel.create(data.song_list);   // 要 .song_list 才是数组
  console.log('插入成功,共', res.length, '条');

  // ===== 二、删除 =====
  // deleteOne:只删除满足条件的「第一个」文档
  let r = await SongModel.deleteOne({ author: '林飞如' });
  console.log('deleteOne 删除数量', r.deletedCount);

  // deleteMany:删除满足条件的「所有」文档
  r = await SongModel.deleteMany({ author: '林飞如' });
  console.log('deleteMany 删除数量', r.deletedCount);

  // 条件传空对象 = 清空集合
  // r = await SongModel.deleteMany({});

  // ===== 三、更新 =====
  // updateOne:只改第一个,且只改指定字段,不会用新对象覆盖整个文档
  r = await SongModel.updateOne({ author: '蔡徐坤' }, { author: '高乐高' });
  console.log('updateOne 更新数量', r.modifiedCount);

  // updateMany:改所有满足条件的
  r = await SongModel.updateMany({ author: '蔡徐坤' }, { author: '林飞如' });
  console.log('updateMany 更新数量', r.modifiedCount);

  // ===== 四、查询 =====
  const one = await SongModel.findOne({ author: '高乐高' });   // 单条 -> 对象
  console.log('findOne', one);

  const byId = await SongModel.findById('64a1b2c3d4e5f6a7b8c9d0e1'); // 按 ID 查单条
  console.log('findById', byId);

  const some = await SongModel.find({ author: '高乐高' });      // 多条 -> 数组
  console.log('find 多条数量', some.length);

  const all = await SongModel.find();                          // 不写条件 = 全部
  console.log('全部数量', all.length);

  // 字段筛选 + 排序 + 截取
  const part = await SongModel
    .find({ hot: { $gt: 500 } })        // 条件:热度大于 500
    .select({ title: 1, hot: 1, _id: 0 })  // 1 表示要,0 表示不要(_id 默认会带)
    .sort({ hot: -1 })                  // 1 升序,-1 降序
    .skip(0)                            // 从第几条开始,从 0 开始算
    .limit(10)                          // 取多少条
    .exec();                            // 用 exec 执行
  console.log('筛选排序截取', part);

  mongoose.disconnect();   // 演示脚本才需要关连接,实际项目里不关
});

要点与易错点

  • require('./data.json') 得到的是对象,要取 data.song_list;写 create([]) 就真插入空数组了。
  • 批量插入用 create(数组),返回的 res 是插入后的文档数组,所以会打印一大片。
  • 删除没有 delete 方法,只有 deleteOne / deleteMany;deleteOne 只删第一个,返回值是结果对象,deletedCount 才是数量。
  • 更新用 updateOne / updateMany;因为有 Schema 约束,它只改你给的字段,不会用新对象覆盖整个文档。
  • find() 不写条件就是查全部;findOne 返回对象,find 返回数组。
  • 字段筛选要写成 find(条件).select({...}).exec() 这种链式调用,_id 默认一定返回,想排除必须显式写 _id: 0。
  • sort 里 1 升序、-1 降序;skip 是从 0 开始的下标。
  • 演示脚本里不 disconnect(),Node 进程会一直挂着不退出——这是正常的,实际项目里正需要这样。

3. 记账本项目:在路由里用 Model 做增删查 ​

js
// ===== routes/account.js =====
const express = require('express');
const router = express.Router();

// 第三步:导入 models/account.js,拿到的是 Model 对象
const AccountModel = require('../models/account');

// 首页:查询全部账单
router.get('/', function (req, res) {
  AccountModel.find(function (err, data) {
    if (err) {
      res.status(500).send('数据库读取失败');
      return;
    }
    // data 是数组,直接传给模板
    res.render('index', { list: data });
  });
});

// 添加账单:req.body 正好就是 Schema 里那几个字段,_id 自动生成
router.post('/create', function (req, res) {
  AccountModel.create(req.body, function (err) {
    if (err) {
      res.render('fail', { title: '账单添加失败', url: '/create' });
      return;
    }
    res.render('success', { title: '账单添加成功', url: '/' });
  });
});

// 删除账单:条件用 _id
router.get('/delete/:id', function (req, res) {
  const { id } = req.params;
  AccountModel.deleteOne({ _id: id }, function (err) {
    if (err) {
      res.render('fail', { title: '账单删除失败', url: '/' });
      return;
    }
    res.render('success', { title: '账单删除成功', url: '/' });
  });
});

module.exports = router;
html
<!-- views/index.ejs 里渲染列表的关键一行:用 item._id,不是 item.id -->
<a href="/delete/<%= item._id %>">删除</a>

要点与易错点

  • 路由里 require('../models/account') 得到的是 Model 对象,增删改查全部基于它,不再需要 lowdb 和手动生成 id。
  • 添加时直接 create(req.body) 即可,_id 由 MongoDB 自动生成,不要自己造 ID。
  • 模板里取 ID 必须写 item._id(带下划线),写 item.id 拿不到值,删除链接就带不上 ID。
  • 删除条件要写成 { _id: id },id 是字符串也没关系,Mongoose 会自动转成 ObjectId。
  • 删除/更新失败时 res.render('fail', { title, url }),把标题和跳转地址传给模板复用同一个失败页。
  • 常见报错 xxxModel is not defined,基本都是变量名忘了改;命令行里的报错不友好,要学会顺着信息定位。
  • 想“假删”可以在 Schema 里加 isDelete: { type: Boolean, default: false },删除时改这个字段,查询时只查 isDelete: false。

常见错误 ​

1. 脚本跑完卡住不退出

  • 现象:node 02.js 数据已插入成功,但命令行光标一直停着不返回。
  • 原因:Mongoose 与 MongoDB 的长连接没有关闭,Node 进程认为还有活跃句柄。
  • 修法:演示/脚本场景在回调末尾加 mongoose.connection.close();真实后端服务不要关——服务需要长期待机接请求,连接断了就没法查库。所以这个报错在项目里属于"正常现象",别乱加 close。

2. 批量插入报 xxxModel is not defined

  • 现象:复制上一份代码改成批量插入,一运行就抛引用错误。
  • 原因:复制后只改了 Schema/Model 的定义,忘了改 new mongoose.model(...) 存下来的变量名,调用处还在用旧的 UserModel。
  • 修法:养成"改一处、搜一遍"的习惯:改完 model 变量名后全文搜索旧名字;命令行报错先看最后一行的变量名/文件行号,不要一报错就重写。

3. delete is not a function

  • 现象:调用 Model.delete(条件, cb) 直接报方法不存在。
  • 原因:Mongoose 没有 delete 这个方法,只有区分单条/多条的 deleteOne / deleteMany。
  • 修法:删一条用 deleteOne,删全部匹配用 deleteMany;顺带记住 deleteOne 只删第一个匹配文档,返回的是 { deletedCount } 结果对象,不是被删的数据。

4. 加了 select() 之后回调不执行

  • 现象:find(条件).select({...})(回调) 或直接跟回调,什么也没打印。
  • 原因:select / sort / skip / limit 都是链式查询构造器,它们返回的仍是 Query 对象,不负责执行;把回调塞给 select 是无效的。
  • 修法:整条链写完,最后统一 .exec(callback) 执行。链式调用是"先描述、后执行"。

5. 分页少一条 / 首条丢失

  • 现象:skip(90).limit(10) 拿到的不是第 91~100 条,或者总数对不上。
  • 原因:skip 的参数是索引,从 0 开始计数,不是从 1 开始。
  • 修法:要第 91 条起,写 skip(90);分页公式 skip((page-1)*pageSize)。

6. 删除按钮点了没反应 / id 传不过去

  • 现象:模板里 href="/account/delete?id={{item.id}}",后端拿到的 id 是空。
  • 原因:MongoDB 文档主键是 _id,不是 id;模板取错字段,链接里就没有值。
  • 修法:模板里统一写 item._id;后端用 req.query.id 拼成 {_id: id} 作为删除条件。

7. 数据库还没连上就启动了 HTTP 服务

  • 现象:服务能起来,但一访问就报查询超时/空数据。
  • 原因:app.listen 写在了连接代码外面,连接是异步的,服务先跑起来了。
  • 修法:把 listen 整段代码搬进 mongoose.connection.on('open', ...) 的回调里;连接失败事件里只打印错误并退出,不启动服务。

8. 在路由里连数据库

  • 现象:本地测没问题,一上线连接数暴涨、服务变慢。
  • 原因:连接代码写进了路由回调,每个请求都新建一次连接。
  • 修法:整个应用只连一次,放在启动入口(bin/www),路由层只做 Model.xxx() 的增删改查。

练习 ​

  1. 批量插入 + 字段裁剪:用 require 导入一个 JSON 文件(内含数组),向新集合批量插入数据,Schema 只声明 5 个字段。运行后到图形化工具里确认:多余字段是否被丢弃?_id 是否自动生成?

  2. 方法选型:分别用 deleteOne、deleteMany({}) 清空数据,观察 deletedCount;再用 node 02.js 把数据恢复回来,体会"数据是代码生成的"这件事。

  3. 链式查询:查出"热度 > 500"的数据,只要 title 和 hot 两个字段(不要 _id),按 hot 降序,取第 11~20 条。写完后故意把 .exec() 去掉,看会发生什么,再改回来。

  4. 项目改造:把记账本的 lowdb 全部替换为 Mongoose —— 连接放到启动入口、models/account.js 只导出 Model、路由里用 Model 完成增/查/删,并让首页列表和删除链接都正常工作。

  5. 思考题(写出方案,不必编码):产品要求"删除后 30 天内可恢复"。说明你会怎么改 Schema 和查询条件,以及首页列表、删除接口各需要改动哪里。

原视频对照 ​

#视频时长
1mongoose-添加数据10:21
2mongoose-删除数据05:48
3mongoose-更新数据04:42
4mongoose-查询数据13:42
5记账本项目升级-连接数据库15:10
6记账本项目升级-创建schema和model06:04
7记账本项目升级-实现增删查16:42

小结 ​

  1. 增删改查四个动作的写法高度同构:条件对象 + 回调(err, result),把套路记住比记方法名更划算。
  2. 单条与多条是两个不同方法(findOne/find、deleteOne/deleteMany、updateOne/updateMany),选错方法的后果是"只改了一条"或"误伤全表"。
  3. 空对象 {} 是合法条件,意味着"全部"——它既是清库的利器,也是线上事故的常见来源,写之前先想清楚。
  4. Schema 是白名单:没声明的字段进不了库,这让更新变成"局部修改"而不是"整体覆盖",比原生命令行更安全。
  5. 链式查询(select / sort / skip / limit)只是构造,exec 才执行;_id 默认返回,要排除就显式置 0。
  6. 连接的生命周期应与应用一致:启动时连一次、连接成功后再监听端口、失败就不启动,绝不在路由里连。
  7. 模型文件只负责"定义 Schema + 创建 Model + 导出 Model",谁需要谁导入;这一层分离让后续加用户集合、加登录时不用动路由结构。
  8. 真实业务几乎不做物理删除,多用标记位做逻辑删除——留数据就是留退路,也是后续做恢复、审计、对账的前提。