1

I am trying a sample that uses addtoset to update an array inside a collection. The new elements are being added but not as intended. According to addtoset a new element is added only if it is not in the list.

Issue:

It is simply taking whatever element is being added.

here is my code sample

Schema(mongo_database.js):

    var category = new Schema({
    Category_Name: { type: String, required: true},
    //SubCategories: [{}]
    Category_Type: { type: String},
    Sub_Categories: [{Sub_Category_Name: String, UpdatedOn: { type:Date, default:Date.now} }],
    CreatedDate: { type:Date, default: Date.now},
    UpdatedOn: {type: Date, default: Date.now}

});

service.js

exports.addCategory = function (req, res){
//console.log(req.body);
    var category_name = req.body.category_name;
    var parent_category_id = req.body.parent_categoryId;


            console.log(parent_category_id);    
            var cats = JSON.parse('{ "Sub_Category_Name":"'+category_name+'"}');
            //console.log(cats);
            var update = db.category.update(
                { 
                    _id: parent_category_id
                },
                { 
                    $addToSet: { Sub_Categories: cats}
                },
                {
                    upsert:true
                }
            );

            update.exec(function(err, updation){

            })
    }

Can someone help me to figure this out?

many thanks..

Neil Lunn
  • 148,042
  • 36
  • 346
  • 317
Ramaraju.d
  • 1,301
  • 6
  • 26
  • 46
  • `$addToSet` works on exact matches, so because those elements also contain an `UpdatedAt` field, they won't match and will always be added. See [this question](http://stackoverflow.com/questions/14527980/can-you-specify-a-key-for-addtoset-in-mongo) for a similar question, but it doesn't work with upsert so it's not quite a duplicate. – JohnnyHK Jan 08 '15 at 14:27

1 Answers1

2

As mentioned already, $addToSet does not work this way as the elements in the array or "set" are meant to truly represent a "set" where each element is totally unique. Additionally, the operation methods such as .update() do not take the mongoose schema default or validation rules into account.

However operations such as .update() are a lot more effective than "finding" the document, then manipulating and using .save() for the changes in your client code. They also avoid concurrency problems where other processes or event operations could have modified the document after it was retrieved.

To do what you want requires making "mulitple" update statements to the server. I'ts a "fallback" logic situation where when one operation does not update the document you fallback to the the next:

models/category.js:

var mongoose = require('mongoose'),
    Schema = mongoose.Schema;

var category = new Schema({
    Category_Name: { type: String, required: true},
    Category_Type: { type: String},
    Sub_Categories: [{Sub_Category_Name: String, UpdatedOn: { type:Date, default:Date.now} }],
    CreatedDate: { type:Date, default: Date.now},
    UpdatedOn: {type: Date, default: Date.now}
});

exports.Category = mongoose.model( "Category", category );

in your code:

var Category = require('models/category').Category;

exports.addCategory = function(req,res) {
    var category_name = req.body.category_name;
    var parent_category_id = req.body.parent_categoryId;

    Category.update(
        { 
            "_id": parent_category_id, 
            "Sub_Categories.Sub_Category_Name": category_name
        },
        {
            "$set": { "Sub_Categories.$.UpdatedOn": new Date() }
        },
        function(err,numAffected) {
           if (err) throw error;     // or handle

           if ( numAffected == 0 )
               Category.update(
                   {
                       "_id": parent_category_id, 
                       "Sub_Categories.Sub_Category_Name": { "$ne": category_name }
                   },
                   {
                       "$push": {
                           "Sub_Categories": {
                               "Sub_Category_Name": category_name,
                               "UpdatedOn": new Date()
                           }
                       }
                   },
                   function(err,numAffected) {
                       if (err) throw err;     // or handle

                       if ( numAffected == 0 )
                           Category.update(
                               {
                                   "_id": parent_category_id
                               },
                               { 
                                   "$push": {
                                       "Sub_Categories": {
                                           "Sub_Category_Name": category_name,
                                           "UpdatedOn": new Date()
                                       }
                                   }
                               },
                               { "$upsert": true },
                               function(err,numAffected) {
                                   if (err) throw err;
                               }
                           );
                   });
               );
        }
    );                    
};

Essentially a possible three operations are tried:

  1. Try to match a document where the category name exists and change the "UpdatedOn" value for the matched array element.

  2. If that did not update. Find a document matching the parentId but where the category name is not present in the array and push a new element.

  3. If that did not update. Perform an operation trying to match the parentId and just push the array element with the upsert set as true. Since both previous updates failed, this is basically an insert.

You can clean that up by either using something like async.waterfall to pass down the numAffected value and avoid the indentation creep, or by my personal preference of not bothering to check the affected value and just pass all statements at once to the server via the Bulk Operations API.

The latter can be accessed from a mongoose model like so:

var ObjectId = mongoose.mongo.ObjectID,
   Category = require('models/category').Category;

exports.addCategory = function(req,res) {
    var category_name = req.body.category_name;
    var parent_category_id = req.body.parent_categoryId;


    var bulk = Category.collection.initializeOrderBulkOp();

    // Reversed insert
    bulk.find({ "_id": { "$ne": new ObjectId( parent_category_id ) })
        .upsert().updateOne({
            "$setOnInsert": { "_id": new ObjectId( parent_category_id ) },
            "$push": {
                "Sub_Category_Name": category_name,
                "UpdatedOn": new Date()
            }
        });

    // In place
    bulk.find({ 
        "_id": new ObjectId( parent_category_id ), 
        "Sub_Categories.Sub_Category_Name": category_name
    }).updateOne({
        "$set": { "Sub_Categories.$.UpdatedOn": new Date() }
    });

    // Push where not matched
    bulk.find({
        "_id": new ObjectId( parent_category_id ), 
        "Sub_Categories.Sub_Category_Name": { "$ne": category_name }
    }).updateOne({
        "$push": {
            "Sub_Category_Name": category_name,
            "UpdatedOn": new Date()
        }
    });

    // Send to server
    bulk.execute(function(err,response) {
        if (err) throw err;    // or handle
        console.log( JSON.stringify( response, undefined, 4 ) );
    });
};

Note the reversed logic where the "upsert" occurs first but if course if that succeeded then only the "second" statement would apply, but actually under the Bulk API this would not affect the document. You will get a WriteResult object with the basic information similar to this (in abridged form):

{ "nMatched" : 1, "nUpserted" : 0, "nModified" : 1 }

Or on the "upsert":

{
    "nMatched" : 1,
    "nUpserted" : 1,
    "nModified" : 0,
    "_id" : ObjectId("54af8fe7628bee196ce97ce0")
}

Also note the need to include the ObjectId function from the base mongo driver since this is the "raw" method from the base driver and it does not "autocast" based on schema like the mongoose methods do.

Additionally be very careful with this, because it is a base driver method and does not share the mongoose logic, so if there is no connection established to the database already then calling the .collection accessor will not return a Collection object and the subsequent method calls fail. Mongoose itself does a "lazy" instantation of the database connection, and the method calls are "queued" until the connection is available. Not so with the basic driver methods.

So it can be done, it's just that you need to handle the logic for such array handling yourself as there is no native operator to do that. But it's still pretty simple and quite efficient if you take the proper care.

Neil Lunn
  • 148,042
  • 36
  • 346
  • 317