Best practice for Mongoose updates to database in Node.js? - node.js

Let's say I have the following Schema :
const mySchema = mongoose.Schema({
_id: mongoose.Schema.Types.ObjectId,
date: Number,
data: {
field1 : Number,
field2 : Number
}
});
And I want to update field2 with "myAwesomeValue" for the document having "myAwesomeDate". My current code, inside an async/await function, is :
// V1
var myAwesomeDocument = await myModel.findOneAndUpdate(
{date: myAwesomeDate}, //selector
{$set: { //update
'data.field2': myAwesomeValue
}},
{ //options
upsert: false,
new: true
}
).exec();
This code allows me to work with the updated document.
If I'm not mistaken, the following code has the same behavior, but is to be avoided, since it loads the document to the client side first (therefore less efficient) (Mongoose difference between .save() and using update()) :
// V2
var myAwesomeDocument = await myModel.findOne({date: myAwesomeDate}).exec();
myAwesomeDocument.data.field2 = myAwesomeValue;
myAwesomeDocument = await myAwesomeDocument.save().exec();
Now, I would like to make my code more readable using .doSomething() fashion :
// V3 (any code mistake here ?)
var myAwesomeDocument = await myModel.findOne({date: myAwesomeDate})
.set(
{'data.field2': myAwesomeValue},
{upsert: false, new: true}
)
.exec();
My question is about efficiency first, and then about code readability :
Is there a more efficient code than V1 ?
Does V3 perform the same update ? Is it as efficient as V1 ?
Is there a more efficient and readable way to write V3 ?
Thx for any kind of answer !

From the examples you provided, the most efficient is v1,
V1 Under the hood it triggers only one query, mongo's
findAndModify.
V2 Needs 2 queries to complete the intended update. findOne then
updateOne.
V3 Is not doing the thing intended, and it just does findOne, without
issuing any update operation on the found document.
A correct version of V3, would be the one below:
instance = await model
.findOneAndUpdate({date})
.set({'data.f2': f1},)
.setOptions({new: true})
.exec();
Explanation:
Mongoose findOneAndUpdate returns a Query (check examples). Then we use the Query's methods to set the update operation, and the options.
To conclude, you can go with either V1, or the updated V3 I provided, as they are using the same database calls under the hood.
You can always use mongoose.set('debug': true) to analyze which queries are actually sent to the database.
To back up the things I said above, here is the code snippet I used to run the tests. You can invoke it like:
const uri = 'mongodb://localhost:27017/test-sav';
const mongoose = require('mongoose');
const bombardCount = process.argv[2] ? parseInt(process.argv[2]) : 1;
const debug = process.argv[3] === 'true';
const date = 1234567;
const f1 = 1;
const f2 = 2;
let model;
(async function () {
await mongoose.connect(uri, {useNewUrlParser: true, useUnifiedTopology: true});
const schema = new mongoose.Schema({
date: Number,
data: {
f1: Number,
f2: Number,
}
});
model = mongoose.model('model', schema);
console.log('### START ###')
const doc1 = await bombard(v1, bombardCount);
console.log(doc1);
const doc2 = await bombard(v2, bombardCount);
console.log(doc2);
const doc3 = await bombard(v3, bombardCount);
console.log(doc3);
const doc4 = await bombard(v4, bombardCount);
console.log(doc4);
console.log('### END ###');
})().catch(error => console.error(error)).then(() => process.exit(1));
async function v1() {
console.log('### V1 ###\n');
await beforeEach();
console.time('### V1 ###');
let instance = await model.findOneAndUpdate(
{date},
{
$set: {
'data.f2': f1,
},
},
{
upsert: false,
new: true
}
).exec();
console.timeEnd('### V1 ###');
await afterEach();
return instance;
}
async function v2() {
console.log('### V2 ###\n');
await beforeEach();
console.time('### V2 ###');
let instance = await model.findOne({date}).exec();
instance.data.f2 = f1;
instance = await instance.save();
console.timeEnd('### V2 ###');
await afterEach();
return instance;
}
async function v3() {
console.log('### V3 ###\n');
console.time('### V3 ###');
await beforeEach();
let instance = await model
.findOne({date})
.set(
{'data.f2': f1},
{upsert: false, new: true}
).exec();
console.timeEnd('### V3 ###');
await afterEach();
return instance
}
async function v4() {
console.log('### V4 ###\n');
console.time('### V4 ###');
await beforeEach();
let instance = await model
.findOneAndUpdate({date})
.set({'data.f2': f1})
.setOptions({new: true})
.exec();
console.timeEnd('### V4 ###');
await afterEach();
return instance;
}
async function beforeEach() {
await new model({
date,
data: {
f1,
f2,
},
}).save();
mongoose.set('debug', debug);
}
async function afterEach() {
mongoose.set('debug', debug);
await model.deleteMany({});
}
async function bombard(f, times) {
let x;
for (let i = 0; i < times; i++) {
x = await f();
}
return x;
}
node index.js [repeats?=number] [debug?=true/false]

Related

Saving array of children of a Mongoose Schema and then adding returned IDs to parent

i'm trying to loop over an array of objects, saving them to MongoDB and then add the returned ObjectIds to a parent Schema which then is also saved. I'm at a loss here.
Everything gets saved correctly but the Recipe (parent) apparently is saved before I get the returned ObjectIds of the Tags (children). I feel like I've used the async and await keywords a bit to often.
Can someone help? Code simplified, but I can post more if needed.
Parent Schema:
const recipe = new mongoose.Schema(
{
name: String,
ingredients: [
{
type: mongoose.SchemaTypes.ObjectId,
ref: "Ingredient",
},
],
}
);
Child Schema:
const ingredientSchema = new mongoose.Schema({
value: String,
label: String,
});
Payload:
{
name: "Rezept",
ingredients: [
{
label: "zutat",
value: "Zutat",
},
{
label: "schokolade",
value: "Schokolade",
},
],
};
My router:
recipesRouter.post("/", async (req, res) => {
const { body } = req;
const saveIngredients = async () => {
let ingredientIDs = [];
body.ingredients.map(async (ingredient) => {
const i = new Ingredient({
value: ingredient.value,
label: ingredient.label,
});
const savedIngredient = await i.save();
ingredientIDs.push(savedIngredient._id);
});
return ingredientIDs;
};
const recipe = new Recipe({
name: body.name,
ingredients: (await saveIngredients()) || [],
});
const savedRecipe = await recipe.save();
res.status(201).json(savedRecipe);
});
Returned recipe:
savedRecipe: {
name: 'asd',
ingredients: [],
_id: new ObjectId("62782b45a431e6efb7b8b1a7"),
}
As I said, both ingredients individually and the recipe is saved to the MongoDB after this but not the ingredient IDs in the recipe. The returned recipe has an empty array in ingredients. I guess the recipe is saved too soon before MongoDB can return ObjectIds for the ingredients.
Thanks for any help.
First of all, your post method is an async, so everything inside it is wrapped in a resolved promise automatically.
Do you really need to make your saveIngredients as an async? IMHO, it's better to let the saveIngredients not be in another async.
And then we can remove the empty list, and just wait for the saveIngredients() finish first.
const recipe = new Recipe({
name: body.name,
ingredients: await saveIngredients(),
});
Your guess is correct, the Recipe was saved first because all the conditions are fulfilled because it doesn't need to wait for the saveIngredients since you provided a [] as the default value. And your saveIngredients is run in parallel.
I got it smh. Turns out async in a .map or .foreach doesn't go well. I turned it into a simple for loop. It's still bloated/lot of steps imo but it works!
recipesRouter.post("/", async (req, res) => {
const { body } = req;
const saveIngredients = async () => {
let ingredientIDs = [];
for (let i = 0; i < body.ingredients.length; i++) {
const el = body.ingredients[i];
const ing = new Ingredient({
value: el.value,
label: el.label,
});
const savedIngredient = await ing.save();
ingredientIDs.push(savedIngredient._id);
}
return ingredientIDs;
};
const ingredientIDs = await saveIngredients();
const recipe = new Recipe({
name: body.name,
ingredients: ingredientIDs,
});
const savedRecipe = await recipe.save();
res.status(201).json(savedRecipe);
});

Mongo DB Update data

I want to decrease previours quantity by 1 how can I do this in Node Js Mongo Db
Here is my code:
app.put('/quantityUpdate',async(req,res)=>{
const id = req?.body?.id;
const dec= req?.body?.dec;
const filter = {_id:ObjectId(id)}
// this option instructs the method to create a document if no documents match the filter
const options = { upsert: true };
const updateDoc = {
$set: {
quantity: //I'm stuck in this place
},
};
const result = await products.updateOne(filter, updateDoc, options);
return res.send(result);
})
Instead of $set use $inc. It increments a field by a specified value.
To decrease the value by 1 you change your code to:
const updateDoc = { $inc: { quantity: -1 } }
To get more details, checkout the documentation.

How to access a different schema in a virtual method?

I want to write a virtual (get) method for my MongoDb collection (Parts) which needs to access a different schema: I want it to assert if a document is 'obsolete' according to a timestamp available in a different (Globals) collection:
const partsSchema = new Schema({
...
updatedAt: {
type: Date,
},
...
}, {
toObject: { virtuals: true },
toJSON: { virtuals: true },
});
partsSchema.virtual('obsolete').get(async function() {
const timestamp = await Globals.findOne({ key: 'obsolescenceTimestamp' }).exec();
return this.updatedAt < timestamp.value;
});
But when I do a find, I always get a {} in the obsolete field, and not a boolean value...
const p = await parts.find();
...
"obsolete": {},
...
Is there some way to accomplish my goal?
You can do this, but there are a few obstacles you need to hurdle. As #Mohammad Yaser Ahmadi points out, these getters are best suited for synchronous operations, but you can use them in the way you're using them in your example.
So let's consider what's happening here:
partsSchema.virtual('obsolete').get(async function() {
const timestamp = await Globals.findOne({ key: 'obsolescenceTimestamp' }).exec();
return this.updatedAt < timestamp.value;
});
Since the obsolete getter is an async function, you will always get a Promise in the obsolete field when you query your parts collection. In other words, when you do this:
const p = await parts.find();
You will get this:
...
"obsolete": Promise { <pending> },
...
So besides getting the query results for parts.find(), you also need to resolve the obsolete field to get that true or false result.
Here is how I would write your code:
partsSchema.virtual('obsolete').get(async function() {
const Globals = mongoose.model('name_of_globals_schema');
const timestamp = await Globals.findOne({ key: 'obsolescenceTimestamp' });
return this.updatedAt < timestamp.value;
});
Then when querying it...
parts.findOne({_id: '5f76aee6d1922877dd769da9'})
.then(async part => {
const obsolete = await part.obsolete;
console.log("If obsolete:", obsolete);
})

Update a document and return the updated document

const _id = req.params.id;
let tire = await TireModel.findOne({ _id });
tire = await tire.update({description : "new description"});
console.log(tire);
When I run this piece of code, I want the tire to be the updated one, but instead it returns the result of the operation in the database. I already tried the {new: true} option.
You can use findOneAndUpdate() function.
const filter = { name: 'Jean-Luc Picard' };
const update = { age: 59 };
// `doc` is the document _after_ `update` was applied because of
// `new: true`
let doc = await Character.findOneAndUpdate(filter, update, {
new: true
});
doc.name; // 'Jean-Luc Picard'
doc.age; // 59
More information https://mongoosejs.com/docs/tutorials/findoneandupdate.html
After you find your document with findOne, you can update it's fields, and then use save() method:
const _id = req.params.id;
let tire = await TireModel.findOne({ _id });
if (tire) {
tire.description = "new description";
tire = await tire.save();
console.log(tire);
} else {
//todo
}

Mongoose - updating object inside an array

I have a Schema that looks like this:
const RefSchema = {
active: Boolean,
items: [{}],
};
const TopLevelSchema = new mongoose.Schema({
refs: [RefSchema],
...
}, { timestamps: true });
I'm making an API call to update this one of the refs using its id (below its rid) and some data that's inside the API call:
async function updateRef(id, rid, data) {
// First get the TopLevelSchema by the ID - this is OK
const instance = await this.findById(id).exec();
// Prepare the data:
const $set = _.mapKeys(data, (v, k) => `refs.$.${k}`);
// Update the data
await instance.update(
{ 'refs.id': rid },
{ $set },
);
What's happening is that the data (and e.g. I'm passing { active: true }) is not updated.
What am I doing wrong?
There is no need to first get the TopLevelSchema etc. You can update the child like this:
async function updateRef(rid, data) {
let $set = _.mapKeys(data, (v, k) => `refs.$.${k}`)
await TopLevelSchema.updateOne(
{ 'refs._id' : mongoose.Types.ObjectId(rid) },
{ $set })
}
are you using custom ids? because you should do { '_id': rid } instead { 'refs.id': rid }

Resources