18

Previously I used mongodb with php and to query a database I was using a singleton. This way I instantiated connection only once and then reused it:

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

Than I can create class Cats and have too methods addCat and showCats with something like this:

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

Right now I started to use mongodb with node.js. Mongodb tutorial shows me something like this:

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);
});

Which basically tells me that I have to set up all node operations as a callback inside of connect. Reading similar question the person offers:

You open do MongoClient.connect once when your app boots up and reuse the db object. It's not a singleton connection pool each .connect creates a new connection pool.

But I can not understand how should I use it (for example with my cat class)?

Community
  • 1
  • 1
Salvador Dali
  • 214,103
  • 147
  • 703
  • 753
  • 1
    The basic node driver itself is fairly low level. There are various implementations that abstract this sort of work for you. Just noting this before someone believes that "use mongoose" is an answer. So the question here is, do you really want to work at this low level? If so then it is about how to grab that connection instance so you can use it elsewhere, yes? – Neil Lunn Jul 03 '14 at 07:38
  • 1
    @NeilLunn it does not look so low level to me right now. I also do not need all the schema validation that mongoose offers. Based on my previous experience on answering mongo questions here, I noticed that not all the libraries implement all the functionality. Also based on my previous experience building php/node apps I was able to create decent app with vanilla mongo. Back to your last question: as I understood from here, http://mongodb.github.io/node-mongodb-native/driver-articles/mongoclient.html#mongoclient-connection-pooling I can just pass db to my class methods. – Salvador Dali Jul 03 '14 at 07:58
  • I wasn't intending to advocate a particular implementation but rather pointing out a lot of the work has been done before other "abstracting" libraries. And quite a lot of that work is open source, so there is somewhere to look and see how it is done. I do understand what you want here, and that is a singleton instance from which you can just ask for the connection rather than pass it around everywhere. I really have only said, "other things already do that", so if you don't want to use them you could still borrow some code. – Neil Lunn Jul 03 '14 at 08:07
  • 1
    @NeilLunn if you can point me to some of the libraries that do this, I would be grateful. Right now I know mongoose and mongojs. – Salvador Dali Jul 03 '14 at 08:09
  • 1
    mongoskin and monk have some fairly simplified implementations you can look at. a singleton in Javascript along the lines of your PHP code is not much of a stretch though. And asnwers would not really differ from the general javascript answers that already exist. So I was really just probing around whether you really want to do this. – Neil Lunn Jul 03 '14 at 08:16

5 Answers5

21

Here is what uses async await on singleton. In my 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();

And in all modules that will use the same connection

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;
    }
}
Alexander
  • 1,729
  • 3
  • 19
  • 36
  • I am confused. All tutorials including ones from Mongo always close the connection. I use the similar singleton pattern and do not close the connections. I have run a huge stress test and there were no mongo related issues. So the question is: do we have to close the connection in our business logic with this singleton pattern? – Leos Literak Sep 12 '21 at 19:11
13

Here's one way to do it. You can put your database connection details in a small module, initialize it when your app starts up, then use that module from any other modules that need a database connection. Here's the code I've been using and has been working for me in a rather simple internal application.

file: 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);
}

Then from server.js, when your application is starting up:

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

And when you need to use the database, for example in your "Cat.js" file, you could do something like this:

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);
        }
    });
}

I hope this is at least a little helpful in seeing a different approach. I don't claim to be an expert and am open to some SO feedback on this, but this solution has worked well for me so far.

Scampbell
  • 1,535
  • 14
  • 24
8

I upvoted Scampbell's solution, but his solution should be enhanced imho. Currently it is not async, both InitDB and GetDB() should have a callback attribute.

So whenever you change a database to connect, it fails, because it returns before it would have a chance to connect to the database. The bug is not present if you connect always to the same database (so return Database.db is always successful)

This is my bugfix/enhancement to his solution:

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);}
    }
  });
};

The same goes to the rest of his functions. Also note the if (callback part, it allows Database.InitDB() to be called without argument in the beginning of app.js/server.js whatever is your main file.

((I should have written my reply as a comment to Scampbell's solution, but I don't have enough reputation to do so. Also kudos to him for his solution, was a nice starting point))

arcol
  • 1,510
  • 1
  • 15
  • 20
1

You can use ES6 Classes to make a real Singleton.

Here an example in 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;
}

You can improve this Class to connect and keep connection inside or connect and disconnect in other class.

1

My full and working example of a singleton class to connect to mongodb in node.js with typescript.

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));
De Bonheur
  • 742
  • 10
  • 13