1. Egg 项目里接 MongoDB,为什么绕不开 egg-mongoose
如果你正在用 Egg 写接口,数据又要落到 MongoDB,那 egg-mongoose 基本是默认选项。它把 Mongoose 的 Schema、Model、连接管理都挂到 Egg 的 app 和 ctx 上,你不用在每个 Service 里手动 require mongoose,也不用自己维护连接池。Egg 启动时会按 config 里的配置连库,请求进来后直接this.ctx.model.Article就能用,这对写业务代码的人来说省掉了很多样板。
这篇面向的是已经有一个能跑起来的 Egg 项目、但还没接数据库,或者接了但只会find一个方法的同学。我会从config.default.js的配置骨架开始,把 model 目录约定、常用增删改查、聚合、分页排序都走一遍,最后给一个能本地验证的请求示例。目标很明确:照着做完,你能在本地跑通一次完整的增删改查闭环,而不是只停留在“装了个插件”。
需要说明的是,Mongoose 本身是操作 MongoDB 的 ODM,egg-mongoose 是 Egg 生态里的封装插件。两者版本要匹配,Egg 2.x 和 Egg 3.x 在插件加载方式上略有差异,下面配置以 Egg 3.x 为主,2.x 我会在排障部分点出来。
2. 前置准备:装插件、开插件、连上库
2.1 安装 egg-mongoose
在项目根目录执行:
npm install egg-mongoose --save如果你用的是 pnpm 或 yarn,对应替换即可。装完后确认package.json的 dependencies 里有egg-mongoose,版本号不用背,能装上就说明源没问题。
2.2 在 plugin.js 里启用插件
Egg 的插件默认不生效,必须在config/plugin.js里显式开启:
// config/plugin.js exports.mongoose = { enable: true, package: 'egg-mongoose', };这一步很多人会漏,结果启动时报ctx.model is undefined,其实不是配置错,是插件根本没加载。
2.3 在 config.default.js 写连接配置
连接信息放在config/config.default.js里,推荐用环境变量兜底,本地开发直接连本机:
// config/config.default.js module.exports = appInfo => { const config = exports = {}; config.mongoose = { url: process.env.EGG_MONGODB_URL || 'mongodb://127.0.0.1:27017/website', options: { serverSelectionTimeoutMS: 5000, maxPoolSize: 40, }, }; return config; };这里有两个点值得说。第一,url里带库名website,Mongoose 会自动切到这个库,不用额外写dbName。第二,maxPoolSize是连接池上限,老版本 Mongoose 写的是poolSize,新版本已经改成maxPoolSize,如果你照旧教程写poolSize会看到弃用警告,功能还能用但不建议。
注意:本地 MongoDB 默认端口是 27017,如果你用 Docker 起库,记得把端口映射出来,否则连接会超时。
2.4 model 目录约定
Egg 约定 model 放在app/model/下,文件名就是 model 名的小写。比如app/model/article.js对应app.model.Article。这个约定不能改,改了加载不到。
// app/model/article.js 'use strict'; module.exports = app => { const mongoose = app.mongoose; const Schema = mongoose.Schema; const ArticleSchema = new Schema({ title: { type: String, required: true }, keywords: { type: String }, sort: { type: Number, default: 0 }, isSetTop: { type: Number, default: 0 }, release: { type: Boolean, default: false }, columnId: { type: Schema.Types.ObjectId }, tags: { type: Array }, updateTime: { type: Date, default: Date.now }, }); return mongoose.model('Article', ArticleSchema); };Schema 里type支持 String、Number、Boolean、Date、ObjectId、Array、Object 等。ObjectId常用于关联其他集合的_id,Array适合标签这类多值字段。default和required是常用约束,能省掉不少业务层校验。
3. 可复制配置:Service 里怎么写增删改查
Egg 里操作数据库一般放在 Service,Controller 只做参数校验和响应。下面按 create、find、updateOne、aggregate 四类给示例。
3.1 新增:create
// app/service/article.js const Service = require('egg').Service; class ArticleService extends Service { async createArticle(payload) { const { ctx } = this; const result = await ctx.model.Article.create({ title: payload.title, keywords: payload.keywords, sort: payload.sort || 0, tags: payload.tags || [], }); return result; } } module.exports = ArticleService;create返回的是插入后的文档对象,里面带_id。如果你传的是数组,它会批量插入并返回数组。
3.2 查询:find 与条件查询
async listArticles(query) { const { ctx } = this; const conditions = {}; if (query.title) { conditions.title = new RegExp(query.title, 'i'); } if (query.minSort !== undefined) { conditions.sort = { $gte: Number(query.minSort) }; } return ctx.model.Article.find(conditions) .sort({ isSetTop: -1, sort: 1, updateTime: -1 }) .skip(Number(query.pageSize) * (Number(query.pageNum) - 1)) .limit(Number(query.pageSize)) .lean(); }这里用了几个常用操作符:$gte大于等于,$lt小于,$ne不等于,$in匹配多个值,$exists判断字段是否存在。正则用new RegExp比字面量更灵活,能动态拼关键词。.lean()返回纯 JS 对象而不是 Mongoose Document,读多写少的列表接口用它性能更好。
3.3 更新:updateOne 与更新修改器
async updateArticle(id, payload) { const { ctx } = this; return ctx.model.Article.updateOne( { _id: id }, { $set: { title: payload.title, updateTime: new Date() }, $inc: { sort: 1 }, } ); }updateOne返回{ acknowledged, modifiedCount, matchedCount },modifiedCount为 0 说明条件没匹配到或者值没变化。常用修改器有$set设置值、$inc自增、$unset删字段、$push往数组加元素、$pull从数组删元素、$addToSet去重添加。数组批量插入可以配合$each。
3.4 聚合:aggregate
统计每个栏目的文章数:
async countByColumn() { const { ctx } = this; return ctx.model.Article.aggregate([ { $match: { release: true } }, { $group: { _id: '$columnId', total: { $sum: 1 } } }, { $sort: { total: -1 } }, ]); }aggregate返回的是普通数组,不走 Mongoose 的 Document 包装。$match相当于 where,$group做分组,$sum累加。聚合管道写起来像流水线,每一步的输出是下一步的输入,复杂统计基本都能拼出来。
4. 验证请求:本地跑通一次闭环
配置和 Service 写完后,加一个 Controller 和路由,用 curl 验证。
// app/controller/article.js const Controller = require('egg').Controller; class ArticleController extends Controller { async create() { const { ctx } = this; const body = ctx.request.body; const result = await ctx.service.article.createArticle(body); ctx.body = { code: 0, data: result }; } async list() { const { ctx } = this; const list = await ctx.service.article.listArticles(ctx.query); ctx.body = { code: 0, data: list }; } } module.exports = ArticleController;// app/router.js module.exports = app => { const { router, controller } = app; router.post('/api/article', controller.article.create); router.get('/api/article', controller.article.list); };启动项目:
npm run dev先插一条:
curl -X POST http://127.0.0.1:7001/api/article \ -H "Content-Type: application/json" \ -d '{"title":"Egg接入Mongoose","keywords":"egg,mongoose","sort":10,"tags":["node","egg"]}'返回里应该能看到_id和title。再查列表:
curl "http://127.0.0.1:7001/api/article?pageNum=1&pageSize=5&minSort=5"如果返回数组里有刚才那条,说明连接、model、Service、路由整条链路都通了。这一步别跳过,很多人配置写完不验证,等到业务报错才回头查,成本更高。
5. 本篇常见错排查
报错ctx.model is undefined:九成是config/plugin.js没开插件,或者 model 文件没放在app/model/下。Egg 是按目录约定加载的,路径错了不会报文件不存在,只会静默不注册。
连接超时MongooseServerSelectionError:先确认 MongoDB 进程在跑,mongosh能连上;再确认url里的 host 和端口对。Docker 场景常见的是容器内用了127.0.0.1,应该换成容器名或宿主机 IP。
poolSize弃用警告:Mongoose 6 以后连接池参数改成maxPoolSize,把options.server.poolSize换成顶层maxPoolSize即可。
updateOne返回modifiedCount: 0:要么条件没匹配到,要么新值和旧值一样。可以先用findOne确认数据存在,再检查$set的字段是否真的变了。
Egg 2.x 与 3.x 差异:2.x 的插件配置和 model 加载基本一致,但部分中间件和appInfo用法不同。如果你从 2.x 升级,重点看config.default.js的导出方式,3.x 推荐module.exports = appInfo => {}。
聚合结果为空:$match里的字段名要带$前缀引用文档字段,比如$columnId,写成columnId会被当成字符串常量,结果自然不对。
6. 把模型调用接到稳定的 API 通道上
本地跑通之后,下一步通常是把这些 model 调用接到真实的模型服务或业务接口上。这时候请求的稳定性和 Key 管理就变得重要。我一般会把模型调用统一走 TaoToken 的 API 通道,地址是 https://taotoken.net/api ,Key 在控制台生成,接入文档里有各语言的示例,照着改 base_url 就行。
如果你只是先验证模型返回,可以直接用模型对话页面试一条请求,确认通道通了再写进代码。长期做编码或 Agent 类项目的话,Coding Plan 更适合,额度和调用方式都按开发场景设计。控制台里可以管理 API Keys,接入文档在 https://taotoken.net/doc ,ClaudeCodeAnthropic 相关的配置也有单独说明。把这些前置动作做完,再回到 Egg 的 Service 里发请求,排障时就能分清是数据库层的问题还是模型通道的问题,定位会快很多。