如何在NodeJs应用程序和模块之间正确地重复使用与Mongodb的连接

195

我一直在阅读,但仍然不清楚在整个NodeJs应用程序中共享相同数据库(MongoDb)连接的最佳方法是什么。据我所知,在应用程序启动时应该打开连接,并在模块之间重复使用。我目前认为最好的方法是,server.js (一切从此开始的主文件)连接到数据库并创建对象变量,然后将其传递给模块。一旦连接成功,模块代码将根据需要使用此变量,并且此连接保持打开状态。例如:

    var MongoClient = require('mongodb').MongoClient;
    var mongo = {}; // this is passed to modules and code

    MongoClient.connect("mongodb://localhost:27017/marankings", function(err, db) {
        if (!err) {
            console.log("We are connected");

            // these tables will be passed to modules as part of mongo object
            mongo.dbUsers = db.collection("users");
            mongo.dbDisciplines = db.collection("disciplines");

            console.log("aaa " + users.getAll()); // displays object and this can be used from inside modules

        } else
            console.log(err);
    });

    var users = new(require("./models/user"))(app, mongo);
    console.log("bbb " + users.getAll()); // not connected at the very first time so displays undefined

然后另一个模块models/user看起来像这样:

Users = function(app, mongo) {

Users.prototype.addUser = function() {
    console.log("add user");
}

Users.prototype.getAll = function() {

    return "all users " + mongo.dbUsers;

    }
}

module.exports = Users;

现在我有一种可怕的感觉,认为这样做是错误的,那么这个方法是否存在明显的问题,如果有,如何改进呢?


几天前我问过同样类型的问题。https://dev59.com/uGAf5IYBdhLWcg3wizEE - Salvador Dali
请查看 mongoist 驱动程序。它是 "专为 async/await 设计" 的,并允许像 module.exports = mongoist(connectionString); 这样懒惰地导出连接。(在 MongoDB 手册中阅读有关 connectionString 的内容。) - Nil
25个回答

193

您可以创建一个mongoUtil.js模块,其中包含连接到Mongo的功能和返回Mongo数据库实例的函数:

const MongoClient = require( 'mongodb' ).MongoClient;
const url = "mongodb://localhost:27017";

var _db;

module.exports = {

  connectToServer: function( callback ) {
    MongoClient.connect( url,  { useNewUrlParser: true }, function( err, client ) {
      _db  = client.db('test_db');
      return callback( err );
    } );
  },

  getDb: function() {
    return _db;
  }
};

要使用它,你需要在你的app.js中这样做:


To use it, you would do this in your app.js:
var mongoUtil = require( 'mongoUtil' );

mongoUtil.connectToServer( function( err, client ) {
  if (err) console.log(err);
  // start the rest of your app here
} );

接着,当你需要在其他地方访问MongoDB,例如在另一个.js文件中时,可以这样做:

var mongoUtil = require( 'mongoUtil' );
var db = mongoUtil.getDb();

db.collection( 'users' ).find();

这个方法之所以可行,是因为在 node 中,当模块被 require 时,它们只会被加载/引用一次,因此你最终只会得到一个 _db 实例,并且 mongoUtil.getDb() 总是返回同一个实例。

注意:代码未经测试。


7
好的例子!不过,我有一个问题。如果在多个集群中运行您的应用程序,那么该怎么做?它会启动另一个连接实例,还是只使用来自源的现有连接? - Farhan Ahmad
30
如果Mongo的连接在中途断开,你会如何处理?在这种情况下,所有调用getDb()的操作都会失败,直到节点应用程序重新启动。 - Ayan
6
我尝试了这段代码,但在执行 mongoUtil.getDb() 时得到了 null 的结果,我不知道为什么会出现这种情况。 - Keming
5
请确保所有使用mongoUtil模块的模块都在connectToServer回调函数内的app.js中导入。如果你在_db设置之前在app.jsrequire它们,其他模块将会出现未定义错误。 - Mike R
2
从mongoDB版本4开始,应该是var database = mongoUtil.getDb(); database.db().collection( 'users' ) - Julian Veerkamp
显示剩余10条评论

59

有许多方法可以调整以接受配置对象,但总体上它与您的代码布局类似,只是使用了更现代的JS语法。如果您的要求是使用原型和回调函数,可以轻松地进行重写。

mongo.js

const { MongoClient } = require('mongodb');
const config = require('./config');
const Users = require('./Users');
const conf = config.get('mongodb');

class MongoBot {
  constructor() {
    const url = `mongodb://${conf.hosts.join(',')}`;

    this.client = new MongoClient(url, conf.opts);
  }
  async init() {
    await this.client.connect();
    console.log('connected');

    this.db = this.client.db(conf.db);
    this.Users = new Users(this.db);
  }
}

module.exports = new MongoBot();

用户.js

class User {
  constructor(db) {
    this.collection = db.collection('users');
  }
  async addUser(user) {
    const newUser = await this.collection.insertOne(user);
    return newUser;
  }
}
module.exports = User;

应用程序.js

const mongo = require('./mongo');

async function start() {
  // other app startup stuff...
  await mongo.init();
  // other app startup stuff...
}
start();

someFile.js

const { Users } = require('./mongo');

async function someFunction(userInfo) {
  const user = await Users.addUser(userInfo);
  return user;
}

1
这是我遇到的最简洁的方法。 - KalenGi
1
我意识到这个答案已经快一年了,我并不指望能得到更多的信息,但是这似乎是我最想使用的方法,然而我却无法从mongo文件中提取出解构后的Users对象,一点头绪都没有。我的文件与你的someFile.js非常相似,但是第4行调用Users.addUser总是失败 - 说Users未定义。我是否漏掉了什么明显的部分? - Rob E.
1
我最终创建了一个新的问题(https://stackoverflow.com/questions/60353529/creating-a-db-service-in-an-mvc-express-app),因为这个问题困扰了我很久。 - Rob E.
2
使用这种方法,能否检测到连接断开的情况? - Nicholas Smith
1
@IglesiasLeonardo 引用比我聪明的人的话说,“这取决于情况”。如果您每秒要处理成千上万个请求,那么您可能不想每次都等待握手,而是会利用连接池。 - EddieDean
显示剩余8条评论

22

以下是我使用现代语法的实现方法,基于go-oleg的示例。我的代码经过测试,可以正常工作。

我在代码中添加了一些注释。

./db/mongodb.js

 const MongoClient = require('mongodb').MongoClient
 const uri = 'mongodb://user:password@localhost:27017/dbName'
 let _db

 const connectDB = async (callback) => {
     try {
         MongoClient.connect(uri, (err, db) => {
             _db = db
             return callback(err)
         })
     } catch (e) {
         throw e
     }
 }

 const getDB = () => _db

 const disconnectDB = () => _db.close()

 module.exports = { connectDB, getDB, disconnectDB }

./index.js

 // Load MongoDB utils
 const MongoDB = require('./db/mongodb')
 // Load queries & mutations
 const Users = require('./users')

 // Improve debugging
 process.on('unhandledRejection', (reason, p) => {
     console.log('Unhandled Rejection at:', p, 'reason:', reason)
 })

 const seedUser = {
     name: 'Bob Alice',
     email: 'test@dev.null',
     bonusSetting: true
 }

 // Connect to MongoDB and put server instantiation code inside
 // because we start the connection first
 MongoDB.connectDB(async (err) => {
     if (err) throw err
     // Load db & collections
     const db = MongoDB.getDB()
     const users = db.collection('users')

     try {
         // Run some sample operations
         // and pass users collection into models
         const newUser = await Users.createUser(users, seedUser)
         const listUsers = await Users.getUsers(users)
         const findUser = await Users.findUserById(users, newUser._id)

         console.log('CREATE USER')
         console.log(newUser)
         console.log('GET ALL USERS')
         console.log(listUsers)
         console.log('FIND USER')
         console.log(findUser)
     } catch (e) {
         throw e
     }

     const desired = true
     if (desired) {
         // Use disconnectDB for clean driver disconnect
         MongoDB.disconnectDB()
         process.exit(0)
     }
     // Server code anywhere above here inside connectDB()
 })
./users/index.js
 const ObjectID = require('mongodb').ObjectID

 // Notice how the users collection is passed into the models
 const createUser = async (users, user) => {
     try {
         const results = await users.insertOne(user)
         return results.ops[0]
     } catch (e) {
         throw e
     }
 }

 const getUsers = async (users) => {
     try {
         const results = await users.find().toArray()
         return results
     } catch (e) {
         throw e
     }
 }

 const findUserById = async (users, id) => {
     try {
         if (!ObjectID.isValid(id)) throw 'Invalid MongoDB ID.'
         const results = await users.findOne(ObjectID(id))
         return results
     } catch (e) {
         throw e
     }
 }

 // Export garbage as methods on the Users object
 module.exports = { createUser, getUsers, findUserById }

你第一个代码片段中的try catch是否必要?connect函数是异步函数。错误已经通过Node风格回调被捕获了。 - shanks
1
这是一个非常敏锐的问题,我很喜欢。如果不在您放置代码的环境中进行更深入的研究,我不能确定。在代码执行过程中,可能会有有限数量的路径。我主要添加它是为了显示可以在那里放置自定义处理程序,并且因为我默认在异步函数中包含try/catch。它只是一个钩子点。不过,好问题。如果您发现其他说明,我会进行更新。 - agm1984
每次调用 getDB() 都会创建新的连接,对吗? - Vinay Pandya

22
如果您正在使用Express,则可以使用mongo-express-req模块,在请求对象中获取数据库连接。
安装:
npm install --save mongo-express-req

server.js

var app = require('express')();

var mongoExpressReq = require('mongo-express-req');
app.use(mongoExpressReq('mongodb://localhost/test'));

routes/users.js

app.get('/', function (req, res, next) {
    req.db // => Db object
});

注意: mongo-express-req 是未维护的 express-mongo-db分支

不认为请求对象应该在其内部具有数据库连接。这真的没有意义。 - DollarAkshay

13

根据已接受答案的经过测试的解决方案:

mongodbutil.js:

var MongoClient = require( 'mongodb' ).MongoClient;
var _db;
module.exports = {
  connectToServer: function( callback ) {
    MongoClient.connect( "<connection string>", function( err, client ) {
      _db = client.db("<database name>");
      return callback( err );
    } );
  },
  getDb: function() {
    return _db;
  }
};

app.js:

var createError = require('http-errors');
var express = require('express');
var path = require('path');
var cookieParser = require('cookie-parser');
var logger = require('morgan');
var app = express();
app.set('views', path.join(__dirname, 'views'));
app.set('view engine', 'ejs');
app.use(logger('dev'));
app.use(express.json());
app.use(express.urlencoded({ extended: false }));
app.use(cookieParser());
app.use(express.static(path.join(__dirname, 'public')));

var mongodbutil = require( './mongodbutil' );
mongodbutil.connectToServer( function( err ) {
  //app goes online once this callback occurs
  var indexRouter = require('./routes/index');
  var usersRouter = require('./routes/users');
  var companiesRouter = require('./routes/companies');
  var activitiesRouter = require('./routes/activities');
  var registerRouter = require('./routes/register');  
  app.use('/', indexRouter);
  app.use('/users', usersRouter);
  app.use('/companies', companiesRouter);
  app.use('/activities', activitiesRouter);
  app.use('/register', registerRouter);  
  // catch 404 and forward to error handler
  app.use(function(req, res, next) {
    next(createError(404));
  });
  // error handler
  app.use(function(err, req, res, next) {
    res.locals.message = err.message;
    res.locals.error = req.app.get('env') === 'development' ? err : {};
    res.status(err.status || 500);
    res.render('error');
  });
  //end of calback
});

module.exports = app;

activities.js -- 一个路由:

var express = require('express');
var router = express.Router();
var mongodbutil = require( '../mongodbutil' );
var db = mongodbutil.getDb();

router.get('/', (req, res, next) => {  
    db.collection('activities').find().toArray((err, results) => {
        if (err) return console.log(err)
            res.render('activities', {activities: results, title: "Activities"})
    });
});

router.post('/', (req, res) => {
  db.collection('activities').save(req.body, (err, result) => {
    if (err) return console.log(err)
    res.redirect('/activities')
  })
});

module.exports = router;

2
这个答案完整且功能正常。 - Ahmad Sharif

11

以下是我在2020年的设置:

./utils/database.js

const { MongoClient } = require('mongodb');

class Mongo {
    constructor () {
        this.client = new MongoClient("mongodb://127.0.0.1:27017/my-app", {
            useNewUrlParser: true,
            useUnifiedTopology: true
        });
    }

    async main () {
        await this.client.connect();
        console.log('Connected to MongoDB');

        this.db = this.client.db();
    }
}

module.exports = new Mongo();

/app.js

const mongo = require('./utils/database');
const express = require('express');

const app = express();

const boot = async () => {
    await mongo.main();
    app.listen(3000);
};

boot();

9

go-oleg说得基本上是对的,但现在你(可能)不想直接使用"mongodb",而是使用一些框架,它会为你做很多“脏活”。

例如,mongoose是最常见的框架之一。这是我们最初的server.js文件中所使用的:

const mongoose = require('mongoose');
const options = {server: {socketOptions: {keepAlive: 1}}};
mongoose.connect(config.db, options);

这是设置所需的全部内容。现在可以在代码中的任何位置使用它。
const mongoose = require('mongoose');

你会得到使用mongoose.connect设置的实例。


3
mongoose是一种ORM。阅读这篇文章以了解可能的陷阱。毫无疑问,当用于开发和学习过程时,ORM非常棒,但不适用于生产环境。请记住这一点。 - Saras Arya
1
Mongoose 还需要模式。我正在使用 MongoDB 包作为与 Neo4j 的多语言持久性的一部分,因此定义所需的文档属性非常方便。 - agm1984

7

由于这个标签带有Express,我想提一下,Express内置了一个在路由之间共享数据的功能。有一个叫做app.locals的对象。我们可以将属性附加到它上面,并从路由内部访问它。您只需在app.js文件中实例化您的mongo连接。

var app = express();

MongoClient.connect('mongodb://localhost:27017/')
.then(client =>{
  const db = client.db('your-db');
  const collection = db.collection('your-collection');
  app.locals.collection = collection;
});
                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                              // view engine setup
app.set('views', path.join(__dirname, 'views'));

现在你可以直接在路由中访问该数据库连接,无需创建和引入其他模块。

app.get('/', (req, res) => {
  const collection = req.app.locals.collection;
  collection.find({}).toArray()
  .then(response => res.status(200).json(response))
  .catch(error => console.error(error));
});

使用这种方法,你可以在应用程序运行期间保持数据库连接打开状态,除非你选择随时关闭它。通过req.app.locals.your-collection轻松访问,无需额外的模块。


我认为这是最干净的方法。我们对这种方法有什么可能的缺点吗?我正在使用它,看起来非常不错,我愿意分享我的经验。 - Priya Ranjan Singh
1
@PriyaRanjanSingh 老实说,我不知道有什么缺点,但我绝不是这方面的专家。在研究后,我发现其他方法很难使用,而我需要更清晰易懂的代码以便自己受益。希望比我更有经验的人能够指出是否存在任何缺点。虽然我已经使用这种方法一段时间了,但没有遇到任何问题,它似乎运行良好。 - Hoppo
1
这真是太棒了,自从我从你那里学到以后,我已经使用它有一段时间了。在更大规模的情况下,如果我们有多个应用程序实例在pm2/forever后面运行,它将不会被共享。目前来看,这是完美的 :) - Priya Ranjan Singh
app.set('views', path.join(__dirname, 'views')); 这行代码是 不必要的,对吗? - fuat
@fuat 这是原始的使用 express 创建的 app.js 文件中的样板代码的一部分。我包含它是为了展示这段代码在 app.js 文件中的插入位置。 - Hoppo

7

我来晚了,但希望这个答案能帮到某些人,这是一个可用的代码:

db.js

const MongoClient = require("mongodb").MongoClient
const urlMongo = "mongodb://localhost:27017"

var db;

function connectToServer( callback ) {
    MongoClient.connect(urlMongo,  { useUnifiedTopology: true , useNewUrlParser: true }, function( err, client ) {
        db  = client.db('auth');
        return callback( err );
    })
}

function getDb() {
    return db
}

module.exports = {connectToServer, getDb}

我们导出一个函数来连接mongo,另一个函数用于获取连接实例。

app.js

const express = require('express')
const app = express()

const mongo = require('./db.js');

mongo.connectToServer( function( err) {
  if (err) console.log(err);
  const auth = require('./modulos')

  app.post('/login', (req, res) => { auth.login(req, res)})
  app.listen(3000, function () { console.log('Corriendo en puerto 3000')})

});

在初始化连接后,我们必须执行 auth 模块的要求,否则 getDb 函数将返回未定义。

module.js

const db = require('../db.js').getDb()
const usuariosCollection = db.collection('usuarios')

function login(req, res){
    usuariosCollection.find({ 'username': 'Fran' }).toArray(function (err, doc) {
        ...
    })
}

5

将连接初始化为Promise:

const MongoClient = require('mongodb').MongoClient
const uri = 'mongodb://...'
const client = new MongoClient(uri)
const connection = client.connect() // initialized connection

然后在需要对数据库执行操作时,调用连接即可:

    // if I want to insert into the database...
    const connect = connection
    connect.then(() => {
        const doc = { id: 3 }
        const db = client.db('database_name')
        const coll = db.collection('collection_name')
        coll.insertOne(doc, (err, result) => {
            if(err) throw err
        })
    })

1
如何在不将“connection”导入每个模块的情况下,在不同路由和中间件之间使用相同的单一连接? - user1063287

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接