The right way to do multiple MongoDB operations - javascript

If I need to perform two or three different operations on a few collections, is there a better way than chaining together find/update operations? For example:
db.collection('contactinfos').findOneAndUpdate(
{ _id: ObjectID(contactID) },
{ $set: { sharedWith } }
).then(response => {
db.collection('users').update(
{ _id: { $in: sharedWith.map(id => ObjectID(id)) } },
{ $addToSet: { hasAccessTo: contactID } },
{ multi: true }
).then(response => {
db.collection('users').update(
{ _id: { $in: notSharedWith.map(id => ObjectID(id)) } },
{ $pull: { hasAccessTo: contactID } },
{ multi: true }
).then(response => {
return res.send({ success: true });
}).catch(err => {
logger.error(`in updating sharing permissions for ${contactID} by user ${_id}`, err);
return res.status(400).send({ reason: 'unknown' });
});
}).catch(err => {
logger.error(`in updating sharing permissions for ${contactID} by user ${_id}`, err);
return res.status(400).send({ reason: 'unknown' });
});
}).catch(err => {
logger.error(`in updating sharing permissions for ${contactID} by user ${_id}`, err);
return res.status(400).send({ reason: 'unknown' });
});
It just seems messy and there has to be some better way of doing it. Furthermore, if there is an error after the first findOneAndUpdate that prevents the other updates from running, then there will be inconsistent data across documents. The documents contain ID references to other documents for faster lookup.
Also, is there a way to catch all errors within a chain of promises?

From your callback hell I can see you do not use response argument of .then() method anywhere. If you do not need results of one query to perform another, consider using Promise.all() method:
const updateContactInfo = db.collection('contactinfos')
.findOneAndUpdate(
{ _id: ObjectID(contactID) },
{ $set: { sharedWith } }
);
const updateUsers = db.collection('users')
.update(
{ _id: { $in: sharedWith.map(id => ObjectID(id)) } }, //hint: use .map(ObjectId) instead.
{ $addToSet: { hasAccessTo: contactID } },
{ multi: true }
);
const updateUsers2 = db.collection('users')
.update(
{ _id: { $in: notSharedWith.map(id => ObjectID(id)) } }, //hint: use .map(ObjectId) instead.
{ $pull: { hasAccessTo: contactID } },
{ multi: true }
);
Promise
.all([updateContactInfo, updateUsers, updateUsers2])
.then((values) => {
const updateContactInfoResult = values[0];
const updateUsersResult = values[1];
const updateUsers2Result = values[2];
return res.send({ success: true });
})
.catch((reason) => {
logger.error(`msg`, reason);
return res.status(400).send({ reason: 'unknown' });
});
Promise.all() will continue executing following .then() only if all the promises do resolve, otherwise it'll fall into the .catch() method. As of error handling, you can easily chain multiple .catch() methods, which is nicely explained here.
If you cannot have any data inconsistency, either:
Get some SQL database with transactions (easier solution)
Look into MongoDB Two-Phase Commit
And if it is acceptable to happen, let's say once per 1kk times, do include checking it's consistency within your app's logic.

Related

MongoDB/Mongoose $pull an Object in an Array where _id of the Object is matching

I have this Schema here
Consider the likedTours which is an Array of Objects (Tours) (ignore position 0).
I want to pull any Objects where the _id of a Tour matches the critiria.
Adding a new Tour upon liking a tour is okay, but on unlike I don't know how to pull that item out.
Here is my function in the Controller in the Node.JS backend
const unlikeTour = async (req, res) => {
try {
TourDB.Tour.findOneAndUpdate(
{ _id: req.params.tourid },
{
$pull: { likedUsers: req.userID },
$inc: { likes: -1 },
}
).exec(async (err, docs) => {
if (!err) {
try {
await UserDB.User.findOneAndUpdate(
{ _id: req.userID },
{ $pull: { 'likedTours._id': docs._id } } //Here I need help
).exec()
return res.status(200).send({ successMessage: 'Tour successfully unliked' })
} catch (err) {
return res.status(500).send({ errorMessage: 'User not found' })
}
} else {
return res.status(500).send({ errorMessage: 'Tour not found' })
}
})
} catch (err) {
return res.status(500).send({ errorMessage: err })
}
}
This method looks for a tour and update it by pulling out the userID and decrement the likes count by -1.
And then I try to find in the UserDB that tour in the likedTours and tried to pull but it doesn't not work like that.
Thanks in advance
you can update as
await UserDB.User.findOneAndUpdate(
{ _id: req.userID },
{ $pull: { likedTours: { _id: docs._id } } } //Here I need help
).exec();
reference: https://docs.mongodb.com/manual/reference/operator/update/pull/

How can i combine two update requests into one?

I now have a code that works.
await Users.update({ selected: false }, { where: { userId: req.body.userId } });
await Users.update(
{
selected: req.body.selected,
descr: req.body.note
},
{
where:
{
entId: req.body.id,
userId: req.body.userId
}
}
);
But what if it is possible to combine these two queries into one? I need the 'selected' and 'note' field that I pass to change conditionally in the table. And all other 'selected' fields inherent to the user in the table became false.
Unfortunately, I did not find anything like that in the documentation. Thank you in advance for your help!
Unfortunately there is no such method like bulkUpdate in Sequelize so you need to call update twice and better to use a transaction to make these two queries as a one atomic operation.
await Sequelize.transaction(async transaction => {
await Users.update({ selected: false }, { where: { userId: req.body.userId }, transaction });
await Users.update(
{
selected: req.body.selected,
descr: req.body.note
},
{
where:
{
entId: req.body.id,
userId: req.body.userId
},
transaction
}
);
});
You can use the sequelize transaction and wrap it up inside try/catch,
// define transaction outside the try/catch so you can rollback if needed
const transaction = await sequelize.transaction();
try {
await Users.update({ selected: false }, { where: { userId: req.body.userId }, transaction })
.then((r) => r)
.catch((e) => {
throw e;
});
await Users.update(
{
selected: req.body.selected,
descr: req.body.note
},
{
where: {
entId: req.body.id,
userId: req.body.userId
},
transaction
}
)
.then((r) => r)
.catch((e) => {
throw e;
});
// always call commit at the end
await transaction.commit();
return true;
} catch (error) {
// always rollback
await transaction.rollback();
console.log(error);
throw error;
}

Node.js - How to use Sequelize transaction

I am a beginner with sequelize and cannot get the transactions to work. Documentation is unclear and makes the following example not able to adapt to my requirements.
return sequelize.transaction(t => {
// chain all your queries here. make sure you return them.
return User.create({
firstName: 'Abraham',
lastName: 'Lincoln'
}, {transaction: t}).then(user => {
return user.setShooter({
firstName: 'John',
lastName: 'Boothe'
}, {transaction: t});
});
}).then(result => {
// Transaction has been committed
// result is whatever the result of the promise chain returned to the transaction callback
}).catch(err => {
// Transaction has been rolled back
// err is whatever rejected the promise chain returned to the transaction callback
});
First I have to insert a tuple in 'Conto', then insert another tuple in 'Preferenze' and finally based on the 'tipo' attribute insert a tuple in 'ContoPersonale' or 'ContoAziendale'.
If only one of these queries fails, the transaction must make a total rollback, commit.
The queries are:
Conto.create({
id: nextId(),
mail: reg.email,
password: reg.password,
tipo: reg.tipo,
telefono: reg.telefono,
idTelegram: reg.telegram,
saldo: saldoIniziale,
iban: generaIBAN()
})
Preferenze.create({
refConto: 68541
})
if (tipo == 0) {
ContoPersonale.create({
nomeint: reg.nome,
cognomeint: reg.cognome,
dataN: reg.datan,
cf: reg.cf,
refConto: nextId()
})
}
else if (tipo == 1) {
ContoAziendale.create({
pIva: reg.piva,
ragioneSociale: reg.ragsoc,
refConto: nextId()
})
}
With a transaction you pass it to each query you want to be part of the transaction, and then call transaction.commit() when you finished, or transaction.rollback() to roll back all the changes. This can be done use thenables however it is clearer when using async/await.
Since none of your queries depend on each other you can also make them concurrently using Promise.all().
thenables (with auto commit)
sequelize.transaction((transaction) => {
// execute all queries, pass in transaction
return Promise.all([
Conto.create({
id: nextId(),
mail: reg.email,
password: reg.password,
tipo: reg.tipo,
telefono: reg.telefono,
idTelegram: reg.telegram,
saldo: saldoIniziale,
iban: generaIBAN()
}, { transaction }),
Preferenze.create({
refConto: 68541
}, { transaction }),
// this query is determined by "tipo"
tipo === 0
? ContoPersonale.create({
nomeint: reg.nome,
cognomeint: reg.cognome,
dataN: reg.datan,
cf: reg.cf,
refConto: nextId()
}, { transaction })
: ContoAziendale.create({
pIva: reg.piva,
ragioneSociale: reg.ragsoc,
refConto: nextId()
}, { transaction })
]);
// if we get here it will auto commit
// if there is an error it with automatically roll back.
})
.then(() => {
console.log('queries ran successfully');
})
.catch((err) => {
console.log('queries failed', err);
});
async/await
let transaction;
try {
// start a new transaction
transaction = await sequelize.transaction();
// run queries, pass in transaction
await Promise.all([
Conto.create({
id: nextId(),
mail: reg.email,
password: reg.password,
tipo: reg.tipo,
telefono: reg.telefono,
idTelegram: reg.telegram,
saldo: saldoIniziale,
iban: generaIBAN()
}, { transaction }),
Preferenze.create({
refConto: 68541
}, { transaction }),
// this query is determined by "tipo"
tipo === 0
? ContoPersonale.create({
nomeint: reg.nome,
cognomeint: reg.cognome,
dataN: reg.datan,
cf: reg.cf,
refConto: nextId()
}, { transaction })
: ContoAziendale.create({
pIva: reg.piva,
ragioneSociale: reg.ragsoc,
refConto: nextId()
}, { transaction })
]);
// if we get here they ran successfully, so...
await transaction.commit();
} catch (err) {
// if we got an error and we created the transaction, roll it back
if (transaction) {
await transaction.rollback();
}
console.log('Err', err);
}

How to validate Sequelize transaction and make it look nice

I am trying to re-learn NodeJS after a couple years of putting it down, so I'm building a small banking website as a test. I decided to use Sequelize for my ORM, but I'm having a bit of trouble sending money between people in a way that I like.
Here was my first attempt:
// myUsername - who to take the money from
// sendUsername - who to send the money to
// money - amount of money to be sent from `myUsername`->`sendUsername`
// Transaction is created to keep a log of banking transactions for record-keeping.
module.exports = (myUsername, sendUsername, money, done) => {
// Create transaction so that errors will roll back
connection.transaction(t => {
return Promise.all([
User.increment('balance', {
by: money,
where: { username: myUsername },
transaction: t
}),
User.increment('balance', {
by: -money,
where: { username: sendUsername },
transaction: t
}),
Transaction.create({
fromUser: myUsername,
toUser: sendUsername,
value: money
}, { transaction: t })
]);
}).then(result => {
return done(null);
}).catch(err => {
return done(err);
});
};
This worked, but it didn't validate the model when it was incremented. I'd like for the transaction to fail when the model does not validate. My next attempt was to go to callbacks, shown here (same function header):
connection.transaction(t => {
// Find the user to take money from
return User
.findOne({ where: { username: myUsername } }, { transaction: t }) .then(myUser => {
// Decrement money
return myUser
.decrement('balance', { by: money, transaction: t })
.then(myUser => {
// Reload model to validate data
return myUser.reload(myUser => {
// Validate modified model
return myUser.validate(() => {
// Find user to give money to
return User
.findOne({ where: { username: sendUsername } }, { transaction: t })
.then(sendUser => {
// Increment balance
return sendUser
.increment('balance', { by: money, transaction: t })
.then(sendUser => {
// Reload model
return sendUser.reload(sendUser => {
// Validate model
return sendUser.validate(() => {
// Create a transaction for record-keeping
return Transaction
.create({
fromUser: myUser.id,
toUser: sendUser.id,
value: money
}, { transaction: t });
});
});
});
});
});
});
});
});
}).then(result => {
return done(null);
}).catch(err => {
return done(err);
});
This works, in that money is still transfered beetween people, but it still doesn't validate the models. I think the reason is that the .validate() and the .reload() methods do not have the ability to add the transaction: t parameter on it.
My question is if there's a way to do validation in a transaction, but I'd also like some help fixing this "callback hell." Again, I haven't done JS in a while, so there are probably better ways of doing this now that I'm just now aware of.
Thanks!
I believe you can't get validations to fire on the Model's increment and decrement and need to have instances. In some sequelize Model methods you can configure validations to run, but it doesn't look like it here
I'd do it like this
module.exports = async function(myUserId, sendUserId, money) {
const transaction = await connection.transaction();
try {
const [myUser, sendUser] = await Promise.all([
User.findById(myUserId, { transaction }),
User.findById(sendUserId, { transaction })
]);
await Promise.all([
myUser.increment('balance', {
by: money,
transaction
}),
myUser.increment('balance', {
by: -money,
transaction
})
]);
await Transaction.create({...}, { transaction })
await transaction.commit();
} catch(e) {
await transaction.rollback();
throw e;
}
}

Node.js looping async calls

I'm busy working on an endpoint for a reporting system. Node being async is giving me issues, although I'd rather not force it to be synchronous.
We're using MongoDB and Mongoose. I've got to query regex over collection A, then for each document that gets returned, query multiple contained documents to populate a JSON object/array to be returned.
I can use populate for most of the data, except the final looped queries which is where the async kicks in and returns my report early. Is there an elegant way to do this? Or should I be splitting into a different function and calling that multiple times to stick to the functions should do only one thing rule?
Example Code:
A.find({ name: regex }).populate({ path: 'B', populate: { path: 'B.C', model: 'C' } }).exec(function(err, A) {
var report = [];
A.map(function(a)){
report[a.name] = [];
D.aggregate([
{
$match: {
id: B._id
}
},
{
$group: {
_id: null,
count: { $sum: 1 }
}
}
], function(err, result) {
C.map(function(c){
report[a.name].push({
'field1': c.field1,
'field2': c.field2,
'field3': c.field3,
'count': result.count
});
});
});
}
return report;
});
The issue here is with the logic / async. Not with the syntax, hence the semi-pseudo code.
Any help or advice would be greatly appreciated.
You need to familiarize yourself with promises, and with async in general.
Because you are returning an array, that's the value you are going to get.
You have a few options when dealing with Async, but in your case, you want to look at two solutions:
// callbacks
getSetOfIDs((err, ids) => {
let remaining = ids.length;
let things = [];
let failed = false;
ids.forEach(id => {
getThingByID(id, (err, thing) => {
if (failed) { return; }
if (err) {
failed = true;
handleFailure(err);
} else {
remaining -= 1;
things.push(thing);
if (!remaining) {
handleSuccess(things);
}
}
});
});
});
Note, I'm not returning things, I'm passing it into a callback.
You can use higher-order functions to clean this sort of thing up.
// cleaned up callbacks
function handleNodeCallback (succeed, fail) {
return function (err, data) {
if (err) {
fail(err);
} else {
succeed(data);
}
};
}
function handleAggregateCallback (succeed, fail, count) {
let items = [];
let failed = false;
const ifNotFailed = cb => data => {
if (!failed) { cb(data); }
};
const handleSuccess = ifNotFailed((item) => {
items.push(item);
if (items.length === count) { succeed(items); }
});
const handleFailure = ifNotFailed((err) => {
failed = true;
fail(err);
});
return handleNodeCallback(handleSuccess, handleFailure);
}
A little helper code later, and we're ready to go:
// refactored callback app code (note that it's much less scary)
getSetOfIDs((err, ids) => {
const succeed = (things) => app.display(things);
const fail = err => app.apologize(err);
if (err) { return fail(err); }
let onThingResponse = handleAggregateCallback(succeed, fail, ids.length);
ids.forEach(id => getThingByID(id, onThingResponse));
});
Note that aside from higher-order functions, I'm never returning anything, I'm always passing continuations (things to do next, with a value).
The other method is Promises
// Promises
getSetOfIDs()
.then(ids => Promise.all(ids.map(getThingByID)))
.then(things => app.display(things))
.catch(err => app.apologize(err));
To really get what's going on here, learn Promises, the Promise.all static method, and array.map().
Both of these sets of code theoretically do the exact same thing, except that in this last case getSetOfIDs and getThingByID don't take callbacks, they return promises instead.
usually in async calls, after return statement any operations are cancelled.
maybe you can return report object only when all is done and well.
A.find({ name: regex }).populate({ path: 'B', populate: { path: 'B.C', model: 'C' } }).exec(function(err, A) {
var report = [];
A.map(function(a)){
report[a.name] = D.aggregate([
{
$match: {
id: B._id
}
},
{
$group: {
_id: null,
count: { $sum: 1 }
}
}
], function(err, result) {
if(err){
return [];
}
var fields = []
C.map(function(c){
fields.push({
'field1': c.field1,
'field2': c.field2,
'field3': c.field3,
'count': result.count
});
});
return fields;
});
}
return report;
});
Just use promises:
A.find({ name: regex }).populate({ path: 'B', populate: { path: 'B.C', model: 'C' } }).exec(function(err, A) {
var report = [];
return Promise.all([
A.map(function(a)){
return new Promise(function(resolve, reject) {
report[a.name] = [];
D.aggregate([{ $match: { id: B._id }},{$group: {_id: null,count: { $sum: 1 }}}],
function(err, result) {
if(err) {
reject(err)
} else {
C.map(function(c){
report[a.name].push({
'field1': c.field1,
'field2': c.field2,
'field3': c.field3,
'count': result.count
});
});
resolve(report)
}
});
}
})])
})
.then(function(report){
console.log(report)
})
.catch(function(err){
console.log(err)
})

Categories