1. Egg 项目接入 MongoDB 与 Mongoose 的完整链路:从连接配置到数据模型落地
在 Node.js 后端选型里,Egg.js 一直是个很稳的框架,它把约定优于配置这件事做得比较彻底,目录结构、插件机制、多环境配置都有固定套路。而 MongoDB 作为文档型数据库,配合 Mongoose 这个 ODM,能让你用接近写对象的方式去操作数据。把这两者接起来,Egg 官方生态里有个插件叫 egg-mongoose,专门干这件事。
这篇要聊的就是这条链路怎么跑通:插件怎么装、config.default.js 里连接参数怎么写、plugin.js 怎么声明、Schema 怎么定义、Model 怎么挂载到 app 和 ctx 上,最后怎么用接口验证读写真的成功了。适合刚接触 Egg 或者第一次在 Egg 里接 MongoDB 的同学,也适合之前用过但配置老是报错、想系统梳理一遍的人。我会把单库和多库两种配置都写出来,再给一个带索引、时间戳、虚拟值的 User 模型示例,最后附上启动日志和 curl 验证动作,你照着敲基本能跑通。
先说清楚一个概念,Mongoose 里的 Schema 是数据结构的描述,Model 是由 Schema 编译出来的构造函数,真正对数据库做增删改查的是 Model。Egg 的 egg-mongoose 插件做的事,就是把 Mongoose 实例挂到 app 上,让你在 model 目录里直接app.mongoose拿到它,同时把 model 目录下的文件自动加载成app.model.xxx和ctx.model.xxx。理解这一点,后面配置就不容易晕。
2. TaoToken 前置准备:模型接入与 API Key 获取
在正式写 Egg 配置之前,先把模型调用这一层准备好。如果你项目里除了数据库读写,还要接大模型做对话、代码补全或者 Agent 编排,TaoToken 可以作为统一的模型接入入口。它提供 OpenAI 兼容的接口形式,Base URL 和 Key 拿到之后,在 Egg 里用 axios 或者 openai 的 SDK 都能直接调。
第一步是注册并登录控制台。打开 https://taotoken.net/api 这个 API 入口,或者从官网 https://taotoken.net/?utm_source=taotoken_aicg_blog_end&utm_medium=csdn&utm_campaign=rewrite&utm_content= 进控制台。登录之后进 API Keys 页面,新建一个 Key,复制出来存到环境变量里,别硬编码进代码。
第二步是确认你要用的模型 ID。在模型对话页面可以先试跑一下,确认这个模型 ID 是可用的,再写进项目配置。模型对话地址是 https://taotoken.net/api/model-chat?utm_source=taotoken_aicg_blog_end&utm_content=model_chat&utm_campaign=rewrite ,在这里发一条消息,能正常返回就说明 Key 和模型都对。
第三步,如果你是要做长期编码或者 Agent 类的任务,可以看下 Coding Plan,地址是 https://taotoken.net/api/coding-plan?utm_source=taotoken_aicg_blog_end&utm_content=coding_plan&utm_campaign=rewrite 。它更适合持续性的编码场景,比单次调用划算。
在 Egg 项目里,我一般把 Key 放在config/config.default.js里通过环境变量读取,比如process.env.TAOTOKEN_API_KEY,然后在 service 层封装一个请求方法。这样本地开发用.env,线上用容器环境变量,不会把 Key 提交到仓库。接入文档在 https://taotoken.net/api/doc?utm_source=taotoken_aicg_blog_end&utm_content=doc&utm_campaign=rewrite ,里面有完整的请求格式和参数说明,写 service 之前扫一眼能省不少调试时间。
这里要提醒一句,TaoToken 是模型接入层,不是数据库,别把它和 MongoDB 的连接配置混在一起。数据库连接走 egg-mongoose,模型调用走 HTTP 请求,两者在 Egg 里是分开的插件和 service,职责清晰。
3. 可复制配置:plugin.js 声明与 config.default.js 连接参数
这一节是核心,配置写对了后面基本不会卡。先装依赖,在项目根目录执行:
npm install egg-mongoose --save然后在config/plugin.js里声明插件:
// config/plugin.js exports.mongoose = { enable: true, package: 'egg-mongoose', };接着是config/config.default.js,单数据库连接这样写:
// config/config.default.js exports.mongoose = { url: process.env.MONGODB_URL || 'mongodb://127.0.0.1:27017/laurel', options: { useNewUrlParser: true, useUnifiedTopology: true, }, };如果你要连多个数据库,就换成 clients 形式:
// config/config.default.js exports.mongoose = { clients: { db1: { url: process.env.MONGODB_URL_DB1 || 'mongodb://127.0.0.1:27017/db1', options: {}, }, db2: { url: process.env.MONGODB_URL_DB2 || 'mongodb://127.0.0.1:27017/db2', options: {}, }, }, };多库模式下,model 里必须通过app.mongooseDB.get('db1')指定用哪个连接,否则会报找不到连接。单库模式则直接用app.mongoose就行。
这里有个容易踩的坑:options里的useNewUrlParser和useUnifiedTopology在新版 Mongoose 里已经默认开启,写了也不报错,但如果你用的 Mongoose 版本较老,不写这两个会有警告。实测下来,保持写上比较省心。
另外,连接字符串里的数据库名laurel要和你在 MongoDB 里实际建的库对应,不然 Mongoose 会自动建一个空库,你查数据时发现是空的,会以为配置没生效。建议先在 MongoDB 里手动建好库和集合,或者至少确认连接后show dbs能看到。
配置写完后,Egg 启动时会自动加载插件并建立连接。你可以在app.js里加一段启动日志确认:
// app.js module.exports = app => { app.beforeStart(async () => { app.logger.info('MongoDB 连接初始化完成'); }); };4. 数据模型落地:Schema 定义、虚拟值与 Model 挂载
配置通了之后,写 model。Egg 约定 model 文件放在app/model/目录下,文件名就是 model 名。先看一个单库模式下的 User 模型:
// app/model/user.js module.exports = app => { const mongoose = app.mongoose; const Schema = mongoose.Schema; const UserSchema = new Schema( { name: { type: String, required: true }, account: { type: String, unique: true }, password: { type: String }, comments: [{ body: String, date: Date }], tags: { type: [String], index: true }, meta: { votes: Number, favs: Number, }, home: { type: String, alias: 'h' }, }, { timestamps: { createdAt: 'createdAt', updatedAt: 'updatedAt' }, } ); UserSchema.virtual('fullName') .get(function () { return `${this.name} (${this.account})`; }); return mongoose.model('User', UserSchema); };多库模式下,model 要改成从指定连接取:
// app/model/user.js module.exports = app => { const mongoose = app.mongoose; const Schema = mongoose.Schema; const conn = app.mongooseDB.get('db1'); const UserSchema = new Schema({ name: { type: String }, account: { type: String, unique: true }, password: { type: String }, }); return conn.model('User', UserSchema); };几个关键点解释一下。timestamps会自动生成createdAt和updatedAt,省得自己写时间字段。alias: 'h'是给字段起别名,查询时用h和home都能命中。virtual定义的fullName不会存进数据库,只在读取时拼接返回,注意用toJSON({ virtuals: true })才能带出来。
Mongoose 默认会把 model 名转成复数作为集合名,User会变成users。如果你不想这样,在model()第三个参数手动指定集合名:
return mongoose.model('User', UserSchema, 'user');这样集合名就是user,不会加 s。这个细节在对接已有数据库时特别重要,不然你会发现在新集合里查不到老数据。
模型挂载后,在 controller 或 service 里通过ctx.model.User访问,在 app 层通过app.model.User访问。Egg 会自动把app/model下的文件加载成 model,不需要手动 require。
5. 验证请求与常见报错排查
配置和模型都写好后,启动项目:
npm run dev正常的话控制台会打印 MongoDB 连接成功和 beforeStart 的日志。如果报MongooseError: connect ECONNREFUSED 127.0.0.1:27017,说明 MongoDB 服务没起来,先确认本地 MongoDB 在跑,或者连接字符串的地址端口对不对。
写个简单的 controller 验证读写:
// app/controller/user.js const Controller = require('egg').Controller; class UserController extends Controller { async create() { const { ctx } = this; const user = await ctx.model.User.create({ name: '测试用户', account: 'test001', password: '123456', }); ctx.body = { code: 0, data: user }; } async list() { const { ctx } = this; const users = await ctx.model.User.find({}); ctx.body = { code: 0, data: users }; } }配好路由后用 curl 验证:
curl -X POST http://127.0.0.1:7001/user/create curl http://127.0.0.1:7001/user/list能返回数据就说明链路通了。下面列几个我实际遇到过的报错和排查方向。
Unknown modifier: $pushAll这个报错,通常出现在老版本 Mongoose 配合新版 MongoDB 时,Schema 选项里加usePushEach: true能绕过,但更推荐升级 Mongoose 版本。
MongooseError: Operation users.findOne() buffering timed out after 10000ms,这个多半是连接没建立成功,或者连接字符串指向的库不对。检查config.default.js里的 url,以及 MongoDB 是否允许该地址连接。
Cannot read property 'model' of undefined,一般是 model 文件里app.mongoose拿不到,说明插件没启用或者 plugin.js 写错了。确认config/plugin.js里exports.mongoose的 enable 是 true。
E11000 duplicate key error,这是唯一索引冲突,比如 account 字段设了 unique,重复插入相同值就会报。要么改值,要么在业务层先查再插。
如果你在项目里同时用了 TaoToken 做模型调用,报401 Unauthorized一般是 Key 没带对或者过期了,去 API Keys 页面重新生成一个。报local proxy failed这种,检查你的请求地址是不是写成了https://taotoken.net/api而不是带具体路径的接口地址。模型返回里如果出现reading choices相关错误,说明响应结构和你解析的字段对不上,去接入文档确认一下返回格式。
6. 语义一致 CTA:把模型能力和数据层串起来
数据库这层跑通之后,你的 Egg 项目就有了持久化能力。接下来如果要做智能问答、内容生成或者代码辅助,可以把 TaoToken 的模型调用封装成一个 service,在 controller 里先查 MongoDB 拿上下文,再调模型生成结果,最后写回数据库。这样数据层和模型层就串起来了。
需要 Key 和接入细节的,去 API Keys 页面 https://taotoken.net/api/api-keys?utm_source=taotoken_aicg_blog_end&utm_content=api_keys&utm_campaign=rewrite 拿,接入文档在 https://taotoken.net/api/doc?utm_source=taotoken_aicg_blog_end&utm_content=doc&utm_campaign=rewrite 。想先试模型效果的,去模型对话 https://taotoken.net/api/model-chat?utm_source=taotoken_aicg_blog_end&utm_content=model_chat&utm_campaign=rewrite 发几条消息看看返回。长期做编码和 Agent 的,Coding Plan 在 https://taotoken.net/api/coding-plan?utm_source=taotoken_aicg_blog_end&utm_content=coding_plan&utm_campaign=rewrite 。
最后说个实用技巧:Egg 的 model 目录支持热加载,改完 Schema 重启 dev 就行,但如果你改了集合名或者索引,记得去 MongoDB 里确认索引有没有真正建上,db.users.getIndexes()看一眼,别等线上查询慢了才发现索引没生效。