使用Node.js和MongoDB设置单例连接

18

之前我使用php和mongodb,为了查询数据库我使用单例模式。这样一来我只需要实例化一次连接,然后就可以重复使用它:

class MDB{
    protected static $instance;
    public static function use(){
        if(!self::$instance) self::$instance = new MongoClient();
        $db = self::$instance->selectDB('DB_name');
        return $db;
    }
}

我可以创建一个名为Cats的类,并拥有两种方法addCat和showCats,代码可能如下:

MDB::use->{'cats'}->insert([...]);
MDB::use->{'cats'}->find([...]);

我现在开始使用node.js和mongodb。 Mongodb教程向我展示了类似于这样的内容:

var MongoClient = require('mongodb').MongoClient;
MongoClient.connect("mongodb://localhost:27017/exampleDb", function(err, db) {
  if(err) { return console.dir(err); }

  var collection = db.collection('test');
  var doc1 = {'hello':'doc1'};
  collection.insert(doc1);
});

基本上,这告诉我我必须在connect内把所有的node操作设置为回调函数。阅读类似的问题,问题中的人提供了以下建议:

当你启动应用程序时,你只需打开一次MongoClient.connect并重复使用db对象。它不是单例连接池,每个.connect创建一个新的连接池。

但我不知道该如何使用它(例如与我的cat类一起)?

1
基本的节点驱动程序本身相当低级。有各种实现可以为您抽象出这种工作。在有人认为“使用mongoose”是一个答案之前,只是注意一下这一点。因此,问题在于,您真的想在这个低级别上工作吗?如果是这样,那么问题就在于如何获取连接实例,以便您可以在其他地方使用它,对吧? - Neil Lunn
1
@NeilLunn,目前看来这并不是很底层。我也不需要mongoose提供的所有模式验证。根据我以前在这里回答mongo问题的经验,我注意到并非所有库都实现了所有功能。还基于我以前构建php/node应用程序的经验,我能够使用vanilla mongo创建出色的应用程序。回到你最后一个问题:根据我从这里理解的http://mongodb.github.io/node-mongodb-native/driver-articles/mongoclient.html#mongoclient-connection-pooling,我只需将db传递给我的类方法即可。 - Salvador Dali
我并不是想倡导某种特定的实现方式,而是指出许多工作已经在其他“抽象化”库中完成。而且其中相当多的工作都是开源的,所以你可以去看看它们是如何实现的。我确实理解你在这里想要什么,那就是一个单例实例,从中你只需请求连接,而不必到处传递它。我只是说,“其他东西已经做到了这一点”,所以如果你不想使用它们,你仍然可以借鉴一些代码。 - Neil Lunn
1
@NeilLunn如果您能指向一些可以做到这一点的库,我将不胜感激。目前我知道mongoose和mongojs。 - Salvador Dali
1
mongoskin和monk有一些相当简化的实现,您可以查看。在JavaScript中,类似于您的PHP代码的单例模式并不是很困难。答案与已经存在的一般JavaScript答案并没有太大区别。所以我只是想探究一下您是否真的想这样做。 - Neil Lunn
6个回答

21

这里展示了如何在单例上使用async await。在我的db.js中。

var MongoClient = require('mongodb').MongoClient;

var DbConnection = function () {

    var db = null;
    var instance = 0;

    async function DbConnect() {
        try {
            let url = 'mongodb://myurl.blablabla';
            let _db = await MongoClient.connect(url);

            return _db
        } catch (e) {
            return e;
        }
    }

   async function Get() {
        try {
            instance++;     // this is just to count how many times our singleton is called.
            console.log(`DbConnection called ${instance} times`);

            if (db != null) {
                console.log(`db connection is already alive`);
                return db;
            } else {
                console.log(`getting new db connection`);
                db = await DbConnect();
                return db; 
            }
        } catch (e) {
            return e;
        }
    }

    return {
        Get: Get
    }
}


module.exports = DbConnection();

并且在所有将使用相同连接的模块中

var DbConnection = require('./db');

async function insert(data) {
    try {
        let db = await DbConnection.Get();
        let result = await db.collection('mycollection').insert(data);

        return result;
    } catch (e) {
        return e;
    }
}

我感到困惑。所有的教程,包括Mongo的教程都会关闭连接。我使用类似的单例模式,并没有关闭连接。我进行了大规模的压力测试,没有发现任何与Mongo相关的问题。所以问题是:在这种单例模式下,我们是否需要在业务逻辑中关闭连接? - Leos Literak

13

这是一种方法。您可以将数据库连接详细信息放在一个小模块中,在应用程序启动时初始化它,然后从需要数据库连接的任何其他模块中使用该模块。这是我一直在使用并且对于我而言在一个相当简单的内部应用程序中运作良好的代码。

文件:DataAccessAdapter.js

var Db = require('mongodb').Db;
var Server = require('mongodb').Server;
var dbPort = 27017;
var dbHost = 'localhost';
var dbName = 'CatDatabase';

var DataBase = function () {
};

module.exports = DataBase;

DataBase.GetDB = function () {
    if (typeof DataBase.db === 'undefined') {
        DataBase.InitDB();
    }
    return DataBase.db;
}

DataBase.InitDB = function () {
    DataBase.db = new Db(dbName, new Server(dbHost, dbPort, {}, {}), { safe: false, auto_reconnect: true });

    DataBase.db.open(function (e, d) {
        if (e) {
            console.log(e);
        } else {
            console.log('connected to database :: ' + dbName);
        }
    });
}

DataBase.Disconnect = function () {
    if (DataBase.db) {
        DataBase.db.close();
    }
}

DataBase.BsonIdFromString = function (id) {
    var mongo = require('mongodb');
    var BSON = mongo.BSONPure;
    return new BSON.ObjectID(id);
}

然后在server.js中,当你的应用程序启动时:

// Startup database connection
require('./DataAccessAdapter').InitDB();

当您需要使用数据库时,例如在您的“Cat.js”文件中,您可以执行以下操作:

var dataAccessAdapter = require('./DataAccessAdapter');

var Cat = function () {
    if (!Cat.db) {
        console.log('Initializing my Cat database');
        Cat.db = dataAccessAdapter.GetDB();
    }
    if (!Cat.CatCollection) {
            console.log('Initializing cats collection');
        Cat.CatCollection = Cat.db.collection('Cats'); // Name of collection in mongo
    }
    return Cat;
}

module.exports = Cat;

Cat.Name = null;
Cat.HasFur = false;

Cat.Read = function (catId, callback) {
    var o_id = dataAccessAdapter.BsonIdFromString(catId);
    Cat.CatCollection.findOne({ '_id': o_id }, function (err, document) {
        if (!document) {
            var msg = "This cat is not in the database";
            console.warn(msg);
            callback(null, msg);
        }
        else {
            callback(document);
        }
    });
}

我希望这至少对看问题的另一种方式有所帮助。我不自诩为专家,并乐于接受一些来自SO的反馈,但迄今为止,这个解决方案对我来说效果很好。


这在新版本的Mongo中不起作用 lib/mongo.js:22 DataBase.db.open(function (e, d) { ^TypeError: DataBase.db.open不是一个函数 - Reza Bojnordi
这是哪个版本?这篇文章相当旧了,所以我们应该在答案的顶部指定这个不起作用的版本。 - Scampbell
从版本3及更高版本开始,它不会响应此代码。 - Reza Bojnordi

8

我赞成Scampbell的解决方案,但是我认为他的方案需要改进。 目前它不是异步的,因此 InitDBGetDB() 都应该有一个回调参数。

所以每当你更改要连接的数据库时,它都会失败,因为它在连接到数据库之前就返回了。 如果始终连接到相同的数据库,则不存在此错误(因此返回Database.db始终成功)

这是我对他的解决方案的错误修复/增强版本:

Database.InitDB = function (callback) {

  if (_curDB === null || _curDB === undefined ||_curDB === '') {
    _curDB = _dbName;
  }

  Database.db = new Db(_curDB,
                   new Server(_dbHost, _dbPort, {}, {}),
                              { safe: false, auto_reconnect: true });

  Database.db.open(function (err, db) {
    if (err) {
        console.log(err);
    } else {
        console.log('connected to database :: ' + _curDB);
        if (callback !== undefined) {callback(db);}
    }
  });
};

同样适用于他的其他功能。还要注意if (callback部分,它允许在app.js/server.js或者你的主文件中,在没有参数的情况下调用Database.InitDB()
(我本应该将我的回复写成对Scampbell解决方案的评论,但是我没有足够的声望来这么做。同时向他致敬,他的解决方案是一个不错的起点)。

1
你可以使用 ES6 Classes 来创建一个真正的 Singleton。
以下是 Typescript 的示例:
import { MongoClient } from "mongodb";

class MongoSingleton {
    private static mongoClient: MongoClient;

  static isInitialized(): boolean {
    return this.mongoClient !== undefined;
  }

  static getClient(): MongoClient {
    if (this.isInitialized()) return this.mongoClient;

    // Initialize the connection.
    this.mongoClient = new MongoClient(mongoUri, {
      useNewUrlParser: true,
      useUnifiedTopology: true,
    });
    return this.mongoClient;
}

你可以改进这个类,使其能够在内部连接并保持连接,或者在其他类中连接和断开连接。

1

这是一个完整可用的单例类示例,用于在Node.js中使用TypeScript连接MongoDB。

import {Db, MongoClient, MongoError} from 'mongodb'

// Connexion credentials type 
type MongoDBCredential = {
    dbName: string;
    username: string,
    password: string;
    cluster: string;
}

// Singleton DBInstance Class 
export class DbInstance {
 private static _instance: DbInstance;
 private _database: Db;
 private _dbClient: MongoClient;

 private constructor() {};

 public static async getInstance(cred: Readonly<MongoDBCredential>): Promise<DbInstance> {
 return new Promise((resolve, reject) => {
  if(this._instance) {
   resolve(this._instance);
  }
  this._instance = new DbInstance();
  this._instance._dbClient = new MongoClient(`mongodb+srv://${cred.username}:${cred.password}@${cred.cluster}.mongodb.net/${cred.dbName}?retryWrites=true&w=majority&readPreference=secondary`, {
    useNewUrlParser: true,
    useUnifiedTopology: true,
  });
  this._instance._dbClient.connect((error: MongoError) => {
     if(error) {
      reject(error);
     }
     this._instance._database = this._instance._dbClient.db(cred.dbName);
     resolve(this._instance);
   });
  });
}
 get db(): Db {
  return DbInstance._instance._database;
 }
 get client(): MongoClient {
  return DbInstance._instance._dbClient;
 }
}

// To use it  
const cred : MongoDBCredential = {  dbName: '***', username: '***', password: '***', cluster: '***' };
DbInstance.getInstance(cred).then((dbi: DbInstance) => {
// do your crud operations with dbi.db
 dbi.db.collection('employee').findOne({'salary': '80K€ '}).then(account => {
  console.info(account);
  dbi.client.close().
 });
}).catch((error: MongoError )=> console.error(error));

0
以下解决方案对我起到了作用:
class DataProvider {
  static client; // client singleton
    
  // constructor
  constructor(collection) {
     if (DataProvider.client === undefined) {
       DataProvider.client = new MongoClient(config.MONGO_DB_CONNECTION);
       DataProvider.client.on('serverClosed', evt => {
         console.log(`[MONGO DB] serverClosed : ${JSON.stringify(evt, null, 2)}`);
       });
     }
     this.dbName = DB_NAME;
     this.collection = COLLECTION_NAME;
   }
      
   // get method
   get = async ( id) => {
   try {
     await DataProvider.client.connect();
     let res = await DataProvider.client.db(this.dbName).collection(this.collection).findOne({ "_id": 
 new ObjectId(id) });
     const doc = res;
     return doc;
   } catch(err) {
     throw err;
   } finally {
     await DataProvider.client.close();
   }
 }
}

每个继承自DataProvider的类都使用单个数据库客户端实例。

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