连接

您可以使用 mongoose.connect() 方法连接到 MongoDB。

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

这是连接在默认端口 (27017) 上本地运行的 myapp 数据库所需的最低限度。对于本地 MongoDB 数据库,我们建议使用 127.0.0.1 而不是 localhost。这是因为 Node.js 18 及更高版本更喜欢 IPv6 地址,这意味着在许多机器上,Node.js 将解析 localhost 为 IPv6 地址 ::1,而 Mongoose 无法连接,除非 mongodb 实例使用 ipv6 启用。

您还可以在 uri 中指定几个其他参数

mongoose.connect('mongodb://username:password@host:port/database?options...');

有关更多详细信息,请参阅 mongodb 连接字符串规范

操作缓冲

Mongoose 允许您立即开始使用模型,而无需等待 mongoose 建立与 MongoDB 的连接。

mongoose.connect('mongodb://127.0.0.1:27017/myapp');
const MyModel = mongoose.model('Test', new Schema({ name: String }));
// Works
await MyModel.findOne();

这是因为 mongoose 在内部缓冲模型函数调用。这种缓冲很方便,但也经常成为混淆的根源。如果您在没有连接的情况下使用模型,Mongoose 不会 默认抛出任何错误。

const MyModel = mongoose.model('Test', new Schema({ name: String }));
const promise = MyModel.findOne();

setTimeout(function() {
  mongoose.connect('mongodb://127.0.0.1:27017/myapp');
}, 60000);

// Will just hang until mongoose successfully connects
await promise;

要禁用缓冲,请关闭 模式上的 bufferCommands 选项。如果您启用了 bufferCommands 并且连接挂起,请尝试关闭 bufferCommands 以查看是否正确打开了连接。您也可以在全局禁用 bufferCommands

mongoose.set('bufferCommands', false);

请注意,如果您使用 autoCreate 选项,缓冲还负责等待 Mongoose 创建集合。如果您禁用缓冲,您也应该禁用 autoCreate 选项,并使用 createCollection() 创建 固定大小集合具有排序规则的集合

const schema = new Schema({
  name: String
}, {
  capped: { size: 1024 },
  bufferCommands: false,
  autoCreate: false // disable `autoCreate` since `bufferCommands` is false
});

const Model = mongoose.model('Test', schema);
// Explicitly create the collection before using it
// so the collection is capped.
await Model.createCollection();

错误处理

Mongoose 连接可能会出现两类错误。

  • 初始连接错误:如果初始连接失败,Mongoose 将发出 'error' 事件,并且 mongoose.connect() 返回的 promise 将被拒绝。但是,Mongoose 不会 自动尝试重新连接。
  • 初始连接建立后发生的错误:Mongoose 将尝试重新连接,并且会发出 'error' 事件。

要处理初始连接错误,您应该使用 .catch()try/catch 与 async/await 一起使用。

mongoose.connect('mongodb://127.0.0.1:27017/test').
  catch(error => handleError(error));

// Or:
try {
  await mongoose.connect('mongodb://127.0.0.1:27017/test');
} catch (error) {
  handleError(error);
}

要处理初始连接建立后的错误,您应该监听连接上的错误事件。但是,您仍然需要如上所示处理初始连接错误。

mongoose.connection.on('error', err => {
  logError(err);
});

请注意,如果 Mongoose 失去与 MongoDB 的连接,它不一定发出 'error' 事件。您应该监听 disconnected 事件来报告 Mongoose 何时断开与 MongoDB 的连接。

选项

connect 方法还接受一个 options 对象,该对象将传递给底层 MongoDB 驱动程序。

mongoose.connect(uri, options);

可以在 MongoDB Node.js 驱动程序文档中找到 MongoClientOptions 的完整选项列表。Mongoose 会将选项传递给驱动程序,而不会修改,除了下面解释的一些例外。

  • bufferCommands - 这是一个 mongoose 特定的选项(不会传递给 MongoDB 驱动程序),它会禁用 Mongoose 的缓冲机制
  • user/pass - 用于身份验证的用户名和密码。这些选项是 Mongoose 特定的,它们等效于 MongoDB 驱动程序的 auth.usernameauth.password 选项。
  • autoIndex - 默认情况下,mongoose 在连接时会自动构建在模式中定义的索引。这对于开发很有用,但对于大型生产部署来说并不理想,因为索引构建会导致性能下降。如果您将 autoIndex 设置为 false,mongoose 将不会自动为与该连接关联的任何模型构建索引。
  • dbName - 指定要连接到的数据库,并覆盖连接字符串中指定的任何数据库。如果您无法在连接字符串中指定默认数据库,例如使用 某些 mongodb+srv 语法连接,这很有用。

以下是调整 Mongoose 时一些重要的选项。

  • promiseLibrary - 设置 底层驱动程序的 promise 库
  • maxPoolSize - MongoDB 驱动程序将为该连接保持打开的最大套接字数量。默认情况下,maxPoolSize 为 100。请记住,MongoDB 每次只允许一个套接字进行一个操作,因此如果您发现有一些缓慢的查询阻止了更快的查询继续执行,您可能需要增加此值。请参阅 MongoDB 和 Node.js 中的慢速列车。如果您遇到了 连接限制,您可能需要减少 maxPoolSize
  • minPoolSize - MongoDB 驱动程序将为该连接保持打开的最小套接字数量。MongoDB 驱动程序可能会关闭一段时间内处于非活动状态的套接字。如果您预计您的应用程序会经历长时间的空闲时间,并且希望确保套接字保持打开状态以避免在活动增加时出现慢速列车,您可能需要增加 minPoolSize
  • socketTimeoutMS - MongoDB 驱动程序在初始连接后因不活动而终止套接字之前等待的时间。套接字可能因没有活动或长时间运行的操作而处于非活动状态。socketTimeoutMS 默认值为 0,这意味着 Node.js 不会因不活动而超时套接字。此选项在 MongoDB 驱动程序成功完成连接后传递给 Node.js socket#setTimeout() 函数
  • family - 是否使用 IPv4 或 IPv6 连接。此选项传递给 Node.js 的 dns.lookup() 函数。如果您没有指定此选项,MongoDB 驱动程序将首先尝试 IPv6,如果 IPv6 失败,则尝试 IPv4。如果您的 mongoose.connect(uri) 调用需要很长时间,请尝试 mongoose.connect(uri, { family: 4 })
  • authSource - 使用 userpass 进行身份验证时要使用的数据库。在 MongoDB 中,用户的作用域为数据库。如果您遇到意外的登录失败,您可能需要设置此选项。
  • serverSelectionTimeoutMS - MongoDB 驱动程序将尝试找到一个服务器来发送任何给定操作,并在 serverSelectionTimeoutMS 毫秒内不断重试。如果未设置,MongoDB 驱动程序默认使用 30000(30 秒)。
  • heartbeatFrequencyMS - MongoDB 驱动程序每隔 heartbeatFrequencyMS 发送一个心跳来检查连接的状态。心跳会受到 serverSelectionTimeoutMS 的影响,因此 MongoDB 驱动程序默认会最多重试 30 秒失败的心跳。Mongoose 仅在心跳失败后才发出 'disconnected' 事件,因此您可能希望减少此设置以减少服务器停机时间和 Mongoose 发出 'disconnected' 事件之间的时间。我们建议您不要将此设置低于 1000,心跳过多会导致性能下降。

serverSelectionTimeoutMS

serverSelectionTimeoutMS 选项非常重要:它控制 MongoDB Node.js 驱动程序在出错之前尝试重试任何操作的时间。这包括初始连接,如 await mongoose.connect(),以及对 MongoDB 发出请求的任何操作,如 save()find()

默认情况下,serverSelectionTimeoutMS 为 30000(30 秒)。这意味着,例如,如果您在独立 MongoDB 服务器停机时调用 mongoose.connect(),您的 mongoose.connect() 调用将在 30 秒后才会抛出错误。

// Throws an error "getaddrinfo ENOTFOUND doesnt.exist" after 30 seconds
await mongoose.connect('mongodb://doesnt.exist:27017/test');

类似地,如果您的独立 MongoDB 服务器在初始连接后停机,任何 find()save() 调用将在 30 秒后出错,除非您的 MongoDB 服务器重新启动。

虽然 30 秒似乎很长,但 serverSelectionTimeoutMS 意味着您不太可能在 副本集故障转移 期间看到任何中断。如果您丢失了副本集主服务器,MongoDB Node 驱动程序将确保您在副本集选举期间发送的任何操作最终都会执行,假设副本集选举花费的时间少于 serverSelectionTimeoutMS

为了更快地了解连接失败,您可以将 serverSelectionTimeoutMS 减少到 5000,如下所示。我们不建议减少 serverSelectionTimeoutMS,除非您正在运行独立的 MongoDB 服务器而不是副本集,或者除非您正在使用无服务器运行时,例如 AWS Lambda

mongoose.connect(uri, {
  serverSelectionTimeoutMS: 5000
});

无法独立调整 serverSelectionTimeoutMS 以用于 mongoose.connect() 与查询。如果您想减少查询和其他操作的 serverSelectionTimeoutMS,但仍然长时间重试 mongoose.connect(),您有责任使用 for 循环或 类似 p-retry 的工具 自己重试 connect() 调用。

const serverSelectionTimeoutMS = 5000;

// Prints "Failed 0", "Failed 1", "Failed 2" and then throws an
// error. Exits after approximately 15 seconds.
for (let i = 0; i < 3; ++i) {
  try {
    await mongoose.connect('mongodb://doesnt.exist:27017/test', {
      serverSelectionTimeoutMS
    });
    break;
  } catch (err) {
    console.log('Failed', i);
    if (i >= 2) {
      throw err;
    }
  }
}

回调

connect() 函数还接受一个回调参数并返回一个 promise

mongoose.connect(uri, options, function(error) {
  // Check error in initial connection. There is no 2nd param to the callback.
});

// Or using promises
mongoose.connect(uri, options).then(
  () => { /** ready to use. The `mongoose.connect()` promise resolves to mongoose instance. */ },
  err => { /** handle initial connection error */ }
);

连接字符串选项

您也可以在连接字符串中指定驱动程序选项,作为 URI 中查询字符串部分的参数。这仅适用于传递给 MongoDB 驱动程序的选项。您不能在查询字符串中设置 Mongoose 特定的选项,如 bufferCommands

mongoose.connect('mongodb://127.0.0.1:27017/test?socketTimeoutMS=1000&bufferCommands=false&authSource=otherdb');
// The above is equivalent to:
mongoose.connect('mongodb://127.0.0.1:27017/test', {
  socketTimeoutMS: 1000
  // Note that mongoose will **not** pull `bufferCommands` from the query string
});

将选项放入查询字符串的缺点是查询字符串选项更难阅读。优点是您只需要一个配置选项(URI),而不是 socketTimeoutMS 等的单独选项。最佳实践是将可能在开发和生产之间不同的选项(如 replicaSetssl)放在连接字符串中,并将应该保持不变的选项(如 socketTimeoutMSmaxPoolSize)放在选项对象中。

MongoDB 文档列出了 支持的连接字符串选项的完整列表。以下是一些通常在连接字符串中设置的选项,因为它们与主机名和身份验证信息密切相关。

  • authSource - 使用 userpass 进行身份验证时要使用的数据库。在 MongoDB 中,用户的作用域为数据库。如果您遇到意外的登录失败,您可能需要设置此选项。
  • family - 是否使用 IPv4 或 IPv6 连接。此选项传递给 Node.js 的 dns.lookup() 函数。如果您没有指定此选项,MongoDB 驱动程序将首先尝试 IPv6,如果 IPv6 失败,则尝试 IPv4。如果您的 mongoose.connect(uri) 调用需要很长时间,请尝试 mongoose.connect(uri, { family: 4 })

连接事件

连接继承自 Node.js 的 EventEmitter,并在连接发生变化时发出事件,例如失去与 MongoDB 服务器的连接。以下是连接可能发出的事件列表。

  • connecting:当 Mongoose 开始与 MongoDB 服务器建立初始连接时发出
  • connected: 当 Mongoose 成功地与 MongoDB 服务器建立初始连接时,或当 Mongoose 在失去连接后重新连接时发出。如果 Mongoose 失去连接,可能会多次发出。
  • open: 在 'connected' 之后发出,并且在这个连接的所有模型上执行 onOpen 后发出。如果 Mongoose 失去连接,可能会多次发出。
  • disconnecting: 您的应用程序调用了 Connection#close() 以断开与 MongoDB 的连接。这包括调用 mongoose.disconnect(),它会对所有连接调用 close()
  • disconnected: 当 Mongoose 失去与 MongoDB 服务器的连接时发出。此事件可能是由于您的代码显式关闭连接、数据库服务器崩溃或网络连接问题导致的。
  • close: 在 Connection#close() 成功关闭连接后发出。如果您调用 conn.close(),您将同时收到 'disconnected' 事件和 'close' 事件。
  • reconnected: 如果 Mongoose 失去与 MongoDB 的连接并成功重新连接,则发出此事件。Mongoose 尝试在失去与数据库的连接时 自动重新连接
  • error: 如果连接发生错误,例如由于格式错误的数据或大于 16MB 的有效负载导致的 parseError,则发出此事件。

当您连接到单个 MongoDB 服务器(一个 "独立")时,如果 Mongoose 与独立服务器断开连接,它将发出 disconnected 事件,如果它成功连接到独立服务器,它将发出 connected 事件。在一个 副本集 中,如果 Mongoose 失去与副本集主服务器的连接,它将发出 disconnected 事件,如果它设法重新连接到副本集主服务器,它将发出 connected 事件。

如果您使用 mongoose.connect(),可以使用以下方法来监听上述事件

mongoose.connection.on('connected', () => console.log('connected'));
mongoose.connection.on('open', () => console.log('open'));
mongoose.connection.on('disconnected', () => console.log('disconnected'));
mongoose.connection.on('reconnected', () => console.log('reconnected'));
mongoose.connection.on('disconnecting', () => console.log('disconnecting'));
mongoose.connection.on('close', () => console.log('close'));

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

对于 mongoose.createConnection(),请使用以下方法

const conn = mongoose.createConnection('mongodb://127.0.0.1:27017/mongoose_test');

conn.on('connected', () => console.log('connected'));
conn.on('open', () => console.log('open'));
conn.on('disconnected', () => console.log('disconnected'));
conn.on('reconnected', () => console.log('reconnected'));
conn.on('disconnecting', () => console.log('disconnecting'));
conn.on('close', () => console.log('close'));

关于 keepAlive 的说明

在 Mongoose 5.2.0 之前,您需要启用 keepAlive 选项以启动 TCP 保活 以防止 "connection closed" 错误。但是,从 Mongoose 5.2.0 开始,keepAlive 默认设置为 true,并且从 Mongoose 7.2.0 开始,keepAlive 已被弃用。请从您的 Mongoose 连接中删除 keepAlivekeepAliveInitialDelay 选项。

副本集连接

要连接到副本集,您需要传递一个逗号分隔的主机列表,而不是单个主机。

mongoose.connect('mongodb://[username:password@]host1[:port1][,host2[:port2],...[,hostN[:portN]]][/[database][?options]]' [, options]);

例如

mongoose.connect('mongodb://user:[email protected]:27017,host2.com:27017,host3.com:27017/testdb');

要连接到单个节点副本集,请指定 replicaSet 选项。

mongoose.connect('mongodb://host1:port1/?replicaSet=rsName');

服务器选择

底层的 MongoDB 驱动程序使用一种称为 服务器选择 的过程来连接到 MongoDB 并向 MongoDB 发送操作。如果 MongoDB 驱动程序在 serverSelectionTimeoutMS 后无法找到要发送操作的服务器,您将收到以下错误

MongoTimeoutError: 服务器选择在 30000 毫秒后超时

您可以使用 serverSelectionTimeoutMS 选项配置 mongoose.connect() 的超时时间

mongoose.connect(uri, {
  serverSelectionTimeoutMS: 5000 // Timeout after 5s instead of 30s
});

MongoTimeoutError 具有一个 reason 属性,它解释了服务器选择超时的原因。例如,如果您连接到具有错误密码的独立服务器,reason 将包含 "Authentication failed" 错误。

const mongoose = require('mongoose');

const uri = 'mongodb+srv://username:[email protected]/' +
  'test?retryWrites=true&w=majority';
// Prints "MongoServerError: bad auth Authentication failed."
mongoose.connect(uri, {
  serverSelectionTimeoutMS: 5000
}).catch(err => console.log(err.reason));

副本集主机名

MongoDB 副本集依赖于能够可靠地确定每个成员的域名。
在 Linux 和 OSX 上,MongoDB 服务器使用 hostname 命令 的输出来确定要报告给副本集的域名。如果您连接到运行在报告其 hostnamelocalhost 的机器上的远程 MongoDB 副本集,这可能会导致令人困惑的错误。

// Can get this error even if your connection string doesn't include
// `localhost` if `rs.conf()` reports that one replica set member has
// `localhost` as its host name.
MongooseServerSelectionError: connect ECONNREFUSED localhost:27017

如果您遇到类似的错误,请使用 mongo shell 连接到副本集,并运行 rs.conf() 命令以检查每个副本集成员的主机名。按照 本页面的说明更改副本集成员的主机名

您还可以检查 MongooseServerSelectionErrorreason.servers 属性,以查看 MongoDB Node 驱动程序认为您的副本集的状态是什么。reason.servers 属性包含一个 服务器描述映射

if (err.name === 'MongooseServerSelectionError') {
  // Contains a Map describing the state of your replica set. For example:
  // Map(1) {
  //   'localhost:27017' => ServerDescription {
  //     address: 'localhost:27017',
  //     type: 'Unknown',
  //     ...
  //   }
  // }
  console.log(err.reason.servers);
}

多 mongos 支持

您还可以连接到多个 mongos 实例,以便在分片集群中实现高可用性。您 无需传递任何特殊选项即可连接到多个 mongos 在 mongoose 5.x 中。

// Connect to 2 mongos servers
mongoose.connect('mongodb://mongosA:27501,mongosB:27501', cb);

多个连接

到目前为止,我们已经了解了如何使用 Mongoose 的默认连接来连接到 MongoDB。当您调用 mongoose.connect() 时,Mongoose 会创建一个 *默认连接*。您可以使用 mongoose.connection 访问默认连接。

您可能出于多种原因需要多个连接到 MongoDB。一个原因是如果您有多个数据库或多个 MongoDB 集群。另一个原因是解决 慢速连接mongoose.createConnection() 函数接受与 mongoose.connect() 相同的参数并返回一个新的连接。

const conn = mongoose.createConnection('mongodb://[username:password@]host1[:port1][,host2[:port2],...[,hostN[:portN]]][/[database][?options]]', options);

然后使用此 连接 对象创建和检索 模型。模型**始终**与单个连接相关联。

const UserModel = conn.model('User', userSchema);

createConnection() 函数返回一个连接实例,而不是一个 Promise。如果您想使用 await 来确保 Mongoose 成功连接到 MongoDB,请使用 asPromise() 函数

// `asPromise()` returns a promise that resolves to the connection
// once the connection succeeds, or rejects if connection failed.
const conn = await mongoose.createConnection(connectionString).asPromise();

如果您使用多个连接,您应该确保导出模式,而不是模型。从文件中导出模型称为 *导出模型模式*。导出模型模式的局限性在于您只能使用一个连接。

const userSchema = new Schema({ name: String, email: String });

// The alternative to the export model pattern is the export schema pattern.
module.exports = userSchema;

// Because if you export a model as shown below, the model will be scoped
// to Mongoose's default connection.
// module.exports = mongoose.model('User', userSchema);

如果您使用导出模式模式,您仍然需要在某个地方创建模型。有两种常见的模式。第一个是创建一个函数来实例化一个新的连接并将所有模型注册到该连接上。使用此模式,您也可以使用依赖注入或其他 控制反转 (IOC) 模式 来注册连接。

const mongoose = require('mongoose');

module.exports = function connectionFactory() {
  const conn = mongoose.createConnection(process.env.MONGODB_URI);

  conn.model('User', require('../schemas/user'));
  conn.model('PageView', require('../schemas/pageView'));

  return conn;
};

导出一个创建新连接的函数是最灵活的模式。但是,这种模式可能会让您难以从路由处理程序或您的业务逻辑所在的任何位置访问连接。另一种模式是导出连接并在文件的顶级范围内将模型注册到连接上,如下所示。

// connections/index.js
const mongoose = require('mongoose');

const conn = mongoose.createConnection(process.env.MONGODB_URI);
conn.model('User', require('../schemas/user'));

module.exports = conn;

如果您想为您的 Web API 后端和您的移动 API 后端创建单独的连接,您可以为每个连接创建单独的文件,例如 connections/web.jsconnections/mobile.js。然后,您的业务逻辑可以 require()import 它需要的连接。

连接池

每个 connection,无论是由 mongoose.connect 还是 mongoose.createConnection 创建,都由一个内部可配置的连接池支持,默认最大大小为 100。使用您的连接选项调整池大小

// With object options
mongoose.createConnection(uri, { maxPoolSize: 10 });

// With connection string options
const uri = 'mongodb://127.0.0.1:27017/test?maxPoolSize=10';
mongoose.createConnection(uri);

连接池大小很重要,因为 MongoDB 目前只能处理每个套接字一个操作。因此,maxPoolSize 充当并发操作数量的上限。

多租户连接

在 Mongoose 的上下文中,多租户架构通常是指多个不同的客户端通过单个 Mongoose 应用程序与 MongoDB 通信的情况。这通常意味着每个客户端通过单个 Mongoose 应用程序进行查询和执行更新,但在同一个 MongoDB 集群中拥有不同的 MongoDB 数据库。

我们建议您阅读 这篇关于 Mongoose 的多租户的文章;它很好地描述了我们如何定义多租户,并更详细地概述了我们推荐的模式。

我们推荐两种在 Mongoose 中使用多租户的模式

  1. 维护一个连接池,使用 Connection.prototype.useDb() 方法 在租户之间切换。
  2. 为每个租户维护一个单独的连接池,将连接存储在映射或 POJO 中。

以下是模式 (1) 的示例。我们建议对于租户数量很少或每个租户的工作负载很轻(大约 < 每秒 1 个请求,所有请求的数据库处理时间 < 10 毫秒)的情况使用模式 (1)。模式 (1) 的实现和生产管理更简单,因为只有一个连接池。但是,在高负载下,您可能会遇到一些租户的操作会减慢其他租户的操作的问题,这是由于 慢速连接 导致的。

const express = require('express');
const mongoose = require('mongoose');

mongoose.connect('mongodb://127.0.0.1:27017/main');
mongoose.set('debug', true);

mongoose.model('User', mongoose.Schema({ name: String }));

const app = express();

app.get('/users/:tenantId', function(req, res) {
  const db = mongoose.connection.useDb(`tenant_${req.params.tenantId}`, {
    // `useCache` tells Mongoose to cache connections by database name, so
    // `mongoose.connection.useDb('foo', { useCache: true })` returns the
    // same reference each time.
    useCache: true
  });
  // Need to register models every time a new connection is created
  if (!db.models['User']) {
    db.model('User', mongoose.Schema({ name: String }));
  }
  console.log('Find users from', db.name);
  db.model('User').find().
    then(users => res.json({ users })).
    catch(err => res.status(500).json({ message: err.message }));
});

app.listen(3000);

以下是模式 (2) 的示例。模式 (2) 更灵活,更适合于 > 10k 租户和 > 每秒 1 个请求的用例。因为每个租户都有一个单独的连接池,所以一个租户的缓慢操作对其他租户的影响最小。但是,这种模式在生产中的实现和管理难度更大。特别是,MongoDB 对打开的连接数量有限制,并且 MongoDB Atlas 对打开的连接数量有单独的限制,因此您需要确保连接池中的套接字总数不超过 MongoDB 的限制。

const express = require('express');
const mongoose = require('mongoose');

const tenantIdToConnection = {};

const app = express();

app.get('/users/:tenantId', function(req, res) {
  let initialConnection = Promise.resolve();
  const { tenantId } = req.params;
  if (!tenantIdToConnection[tenantId]) {
    tenantIdToConnection[tenantId] = mongoose.createConnection(`mongodb://127.0.0.1:27017/tenant_${tenantId}`);
    tenantIdToConnection[tenantId].model('User', mongoose.Schema({ name: String }));
    initialConnection = tenantIdToConnection[tenantId].asPromise();
  }
  const db = tenantIdToConnection[tenantId];
  initialConnection.
    then(() => db.model('User').find()).
    then(users => res.json({ users })).
    catch(err => res.status(500).json({ message: err.message }));
});

app.listen(3000);

下一步

现在我们已经介绍了连接,让我们来看看 模型