Reputation: 222621
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
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
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
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
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
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
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
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