Search code examples
node.jsperformancemongoosecode-readability

Best practice for Mongoose updates to database in 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 !


Solution

  • 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]