Salvador Dali
Salvador Dali

Reputation: 222621

Setting up singleton connection with node.js and mongo

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)?

Upvotes: 18

Views: 30349

Answers (7)

Aniruddha Ghosh
Aniruddha Ghosh

Reputation: 93

A singleton is a class that allows only a single instance of itself to be created and gives access to that created instance. In object-oriented programming, it is a design pattern. Here is a code snippet for connecting to MongoDB using the official driver in a singleton class in Node.js.

import { Db, Collection, MongoClient, MongoError } from "mongodb";
import logger from "./logger";
import "dotenv/config";

// Singleton DBInstance Class
export class DBInstance {
    private static instance: DBInstance;
    private static db: Db;

    //Connection Configutation. These are optional
    private opts: object = {
        appname: "name-of-your-project",
        maxIdleTimeMS: 600000, //time a connection can be idle before it's closed.\
        compressors: ["zstd"],
    };

    //Database Credentials
    private MONGODB_URL: string = process.env.MONGODB_URL!;
    private MONGODB_NAME: string = process.env.MONGODB_NAME!;
    private MongoDBClient: MongoClient = new MongoClient(
        this.MONGODB_URL,
        this.opts,
    );

    //Constructor
    private constructor() {
        logger.warn("🔶 New MongoClient Instance Created!!");
    }

    private async initialize() {
        try {
            if (!this.MONGODB_URL || !this.MONGODB_NAME) {
                logger.error("🚫 MongoDB ENV is not set!!");
                process.exit(1);
            }
            const connClient = await this.MongoDBClient.connect();
            DBInstance.db = connClient.db(this.MONGODB_NAME);
            logger.info(`✅ Connected to MongoDB: ${this.MONGODB_NAME}`);
        } catch (err) {
            logger.error("❌ Could not connect to MongoDB\n%o", err);
            throw MongoError;
        }
    }

    //Singleton Function Implement
    public static getInstance = async (): Promise<DBInstance> => {
        if (!DBInstance.instance) {
            DBInstance.instance = new DBInstance();
            await DBInstance.instance.initialize();
        }
        logger.info(`🔄 Old MongoDB instance Called again :)`);
        return DBInstance.instance;
    };

    //Usable Function Component to get data according to Collection Name
    public getCollection = async (collection: string): Promise<Collection> => {
        return DBInstance.db.collection(collection);
    };
}

Now you can use that instance by exporting anywhere in the code. Just like below:

import { DBInstance } from "./mongodb";

export const example = async (): Promise<void> => {
    const collection = await (await DBInstance.getInstance()).getCollection("collection-name");
    
   // Implement your logic with the collection

};

Every time the getInstance is called it will return you the already created instance. The getCollection will help to get a specific collection within a database. Feel free to customize it as needed.

Upvotes: 0

Naveed Ahmed
Naveed Ahmed

Reputation: 1

The following solution worked for me:

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

Every class that inherits from DataProvider, uses single database client instance.

Upvotes: 0

STEVE  K.
STEVE K.

Reputation: 909

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

Upvotes: 1

SEQOY Development Team
SEQOY Development Team

Reputation: 1615

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.

Upvotes: 1

Alexander
Alexander

Reputation: 1829

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

Upvotes: 21

arcol
arcol

Reputation: 1648

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

Upvotes: 8

Scampbell
Scampbell

Reputation: 1575

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.

Upvotes: 13

Related Questions