Mongoose 在 Node.js 里封装了 MongoDB 驱动,默认的 mongoose.connect 会创建一个全局连接,后续通过 mongoose.model 注册的模型都会挂到这个连接上。这在单库项目里没有问题,可是当业务把用户数据和订单数据拆到不同数据库时,继续使用全局连接就会显得别扭。有人会尝试连续调用两次 mongoose.connect,结果发现第二个连接覆盖了第一个,或者模型写入的库并不符合预期。Mongoose 本身对多数据库连接提供了足够的支持,关键是选对创建连接和注册模型的方式。

用 createConnection 建立相互隔离的连接
处理多数据库最直接的方法是使用 mongoose.createConnection。它返回一个独立的 Connection 对象,每个连接都有自己的连接池、事件循环和待执行队列。与全局的 mongoose.connect 不同,多次调用 createConnection 不会互相覆盖,也不会影响 mongoose.models 中的全局模型注册表。比如我们需要让用户模型写入 users 库,让订单模型写入 orders 库,可以先分别创建两个连接。
const mongoose = require('mongoose');
const userConn = mongoose.createConnection('mongodb://127.0.0.1:27017/users');
const orderConn = mongoose.createConnection('mongodb://127.0.0.1:27017/orders');
const userSchema = new mongoose.Schema({
name: { type: String, required: true },
email: { type: String, required: true, unique: true }
});
const orderSchema = new mongoose.Schema({
userId: { type: mongoose.Schema.Types.ObjectId, ref: 'User' },
amount: { type: Number, required: true },
status: { type: String, default: 'pending' }
});
const User = userConn.model('User', userSchema);
const Order = orderConn.model('Order', orderSchema);
async function run() {
await User.create({ name: '张三', email: 'zhangsan@ipipp.com' });
await Order.create({ userId: null, amount: 99.9 });
console.log('两个库写入完成');
}
run().catch(console.error);
这段代码里,User 模型只属于 userConn,Order 模型只属于 orderConn。即使两个模型拥有相同的模型名,也不会发生冲突,因为模型注册表是每个连接独立维护的。调用 User.create 时,Mongoose 会自动使用 userConn 的连接池把数据写入 users 数据库;调用 Order.create 则写入 orders 数据库。这样就不需要手动切换数据库,也不容易因为全局模型缓存而写入错误位置。
还要注意,mongoose.createConnection 创建的连接默认不会自动建立物理连接,而是在第一次操作数据库时触发。为了确认连接真正可用,可以监听对应连接上的事件。连接选项里的 bufferCommands 默认是 true,意思是连接未就绪时模型操作会进入缓冲队列,等连接建立后再执行。这个机制在单连接下能减少启动时序问题,但在多连接场景中,如果某个数据库暂时不可用,缓冲命令可能一直挂起,因此对关键业务最好显式设置 bufferCommands: false 并配合错误处理。
const userConn = mongoose.createConnection('mongodb://127.0.0.1:27017/users', {
bufferCommands: false,
serverSelectionTimeoutMS: 5000
});
userConn.on('connected', () => {
console.log('users 库连接成功');
});
userConn.on('error', (err) => {
console.error('users 库连接失败:', err.message);
});
这里使用 serverSelectionTimeoutMS 控制选主超时,避免长时间等待。监听 error 事件很重要,否则连接失败时可能抛出未捕获异常。独立连接方案适合数据库数量固定、生命周期与进程一致的场景,例如用户库、订单库、日志库分别部署在不同 MongoDB 实例。
使用 useDb 在同一连接上切换数据库
如果多个数据库位于同一个 MongoDB 实例,只是库名不同,那么可以使用 Connection 对象上的 useDb 方法来获得指向另一个库的连接视图。它不会创建新的物理连接池,而是复用当前连接,只是在执行命令时指定不同的数据库名。这种方式比创建多个连接池更轻量,尤其适合数据库数量较多或者需要动态决定目标库的场景。
const mongoose = require('mongoose');
async function main() {
const baseConn = await mongoose.connect('mongodb://127.0.0.1:27017/admin');
const logSchema = new mongoose.Schema({
level: String,
message: String,
createdAt: { type: Date, default: Date.now }
});
const logDb = baseConn.connection.useDb('logs');
const Log = logDb.model('Log', logSchema);
const auditDb = baseConn.connection.useDb('audit');
const AuditLog = auditDb.model('AuditLog', logSchema);
await Log.create({ level: 'info', message: '服务启动' });
await AuditLog.create({ level: 'info', message: '审计记录' });
console.log('logs 和 audit 库写入完成');
}
main().catch(console.error);
上面的例子先使用 mongoose.connect 建立了一个到 admin 库的连接,然后通过 useDb('logs') 和 useDb('audit') 得到两个不同数据库的连接视图,再分别注册模型。虽然底层复用的是一个物理连接,但模型写入的库名已经不同。这样做的好处是资源占用更少,因为不需要维护多个连接池;缺点则是所有数据库共享同一个连接,如果某条命令执行时间较长,可能会影响到同连接上的其他库操作。
需要区分的是,useDb 返回的 Connection 对象和 createConnection 返回的对象在功能上基本一致,都可以调用 model、监听事件、关闭连接。但 useDb 创建的连接视图不能单独关闭物理连接,调用 close 时会关闭它共享的底层连接。因此如果多个库共用连接,关闭时要格外小心,避免误关影响其他业务。
还有一种做法是在同一个连接上注册多个库的模型,但通过 db.model 绑定到不同 db 对象。例如可以从 mongoose.connection.useDb('db1') 和 mongoose.connection.useDb('db2') 生成两个 db 实例,然后分别注册相同的模型名。此时模型查找会优先从该 db 实例的模型缓存中找,不会跨库混淆。这种方案在一些 SaaS 系统中很有用,比如每个租户拥有独立数据库,但连接可以复用。
模型、连接池与缓存之间的关系
Mongoose 的模型并不直接保存数据,它最终会通过对应的 Connection 对象执行 MongoDB 命令。每个 Connection 对象内部维护一个模型注册表,也就是 conn.models。当你调用 conn.model('User', schema) 时,Mongoose 会把模型名和编译后的 Model 构造函数放进这个注册表。之后再次用同一个连接调用 conn.model('User') 就能直接取出缓存,避免重复编译。
多个连接之间的模型缓存是完全隔离的。即使你在 userConn 上注册了 User,在 orderConn 上仍然可以注册另一个 User,并且它们会写入不同的数据库。这个特性有时候会被误解为模型名不能重复,其实限制只存在于同一个连接内部。比如你连续两次在 userConn 上注册同名模型,第二次会覆盖第一次,这在重构代码时需要避免。
// 同一个连接上重复注册同名模型会覆盖
const UserV1 = userConn.model('User', userSchemaV1);
const UserV2 = userConn.model('User', userSchemaV2);
// 此时 userConn.models.User 指向 UserV2
console.log(userConn.models.User === UserV2); // true
在连接池层面,createConnection 会为每个连接创建独立的连接池。默认情况下,MongoDB Node.js 驱动会给每个连接池设置 5 个连接。多数据库项目里如果创建过多连接,可能会占用大量数据库连接数,尤其当客户端数量较多时需要评估。对于同一个 MongoDB 实例上的多个库,使用 useDb 复用连接池通常是更合理的选择;但对于不同实例、不同权限、不同副本集,独立连接则更容易做故障隔离和配置管理。
还有一个容易被忽略的点是事务。MongoDB 的事务需要在同一个会话和连接上执行,因此如果你需要对两个数据库分别开启事务,必须确保它们位于同一个连接对象上。使用 useDb 创建的连接视图可以共享会话,适合做跨库事务;而两个独立的 createConnection 连接无法在同一个事务中操作。当然,跨库事务会带来锁和性能开销,是否使用要根据业务一致性要求评估。
生命周期管理与错误处理
多连接应用需要统一管理连接的状态,否则某个数据库短暂不可用时可能拖垮整个服务。建议在连接创建后立即注册 connected、error、disconnected 事件,并对外暴露状态判断函数。例如健康检查接口可以遍历所有连接,查看 readyState 是否为 1。若某个连接断开,可以记录日志并触发重连策略。
const connections = [userConn, orderConn];
for (const conn of connections) {
conn.on('connected', () => console.log(`连接成功: ${conn.name}`));
conn.on('disconnected', () => console.warn(`连接断开: ${conn.name}`));
conn.on('error', (err) => console.error(`连接错误: ${conn.name}`, err.message));
}
function allReady() {
return connections.every((conn) => conn.readyState === 1);
}
当应用收到 SIGINT 或 SIGTERM 信号时,应该显式关闭所有独立连接。若是 useDb 产生的连接视图,只需要关闭底层连接一次。关闭连接时最好加上超时保护,防止数据库无响应导致进程无法退出。
async function shutdown() {
const closeTasks = [userConn, orderConn].map((conn) => conn.close());
await Promise.allSettled(closeTasks);
console.log('所有数据库连接已关闭');
process.exit(0);
}
process.on('SIGINT', shutdown);
process.on('SIGTERM', shutdown);
错误处理方面不要只依赖全局 mongoose.connection 的事件。对于每个独立连接都要单独监听 error,因为 Node.js 的事件机制里,Error 事件如果没有监听器会抛出异常。多连接时如果一个连接出错而没有任何监听,可能直接导致进程崩溃。正确做法是在创建连接的工具函数里统一绑定错误监听,并输出包含数据库名称的日志,方便定位是哪个库出现问题。
对于查询级别的错误,比如唯一索引冲突、类型校验失败,需要在业务代码中捕获并返回明确信息,而不是让 Promise 一直向上抛。可以在包装模型操作时统一捕获 MongoServerError 和 ValidationError,根据错误码区分处理。多数据库场景中不同库的错误可能有差异,建议在日志中同时记录连接名称和模型名称,便于快速排查。
实践中的目录结构与封装建议
当多数据库连接的代码散落在各个模块中时,后续维护会很麻烦。更清爽的做法是把连接创建和模型注册集中到独立文件。例如在 db 目录下创建 connections.js,统一导出 userConn 和 orderConn;在 models 目录下分别定义 User 和 Order 的 Schema,并传入对应的连接进行注册。这样业务代码只依赖模型,不需要关心底层连接细节。
// db/connections.js
const mongoose = require('mongoose');
const userConn = mongoose.createConnection(process.env.USER_DB_URI, {
bufferCommands: false
});
const orderConn = mongoose.createConnection(process.env.ORDER_DB_URI, {
bufferCommands: false
});
module.exports = { userConn, orderConn };
// models/user.js
const mongoose = require('mongoose');
const { userConn } = require('../db/connections');
const userSchema = new mongoose.Schema({
name: String,
email: { type: String, unique: true }
});
module.exports = userConn.model('User', userSchema);
业务代码里直接 require('../models/user') 就能拿到绑定好连接的模型,调用 create、find 等方法时自动写入对应的库。连接字符串可以放在环境变量中,便于测试环境和生产环境切换。对于需要动态选择数据库的情况,可以把连接或 db 实例放在 Map 中缓存,避免每次请求都重新创建。
使用 createConnection 时还可以为不同连接设置不同的查询选项,例如读写关注、超时时间和重试策略。订单库要求强一致可以设置 writeConcern: { w: 'majority' },日志库可以降低写关注来提升吞吐。不要把所有连接都套用同一套参数,否则可能牺牲部分场景的性能。
总结来说,Mongoose 多数据库连接的核心不是把多个 URL 塞进同一个 connect,而是让每个连接对象独立维护自己的模型注册表和连接池。固定多库优先用 createConnection,同实例多库优先用 useDb,同时要把连接事件、关闭流程和错误处理完整考虑进去。这样才能在拆分数据存储时保持代码清晰,避免串库和连接泄漏。
Mongoose多数据库Mongoose连接池模型绑定修改时间:2026-10-06 21:44:48