3

I'm using Mongoose (MongoDB in node.js), and after reading this answer:

I have another question:

  • Is it possible to do in the same sentence: push element into array or replace if this element is existing in the array?

Maybe something like this? (The example doesn't work)

    Model.findByIdAndUpdate(id,
    {
     $pull: {"readers": {user: req.user.id}},
     $push:{"readers":{user: req.user.id, someData: data}}
    },{multi:true},callback)

Message error:

errmsg: 'exception: Cannot update \'readers\' and \'readers\' at the same time

Reference:

Thank you!

1 Answer 1

4

Multiple operations on the same property path are simply not allowed in a single request, with the main reason being that the operations themselves have "no particular order" in the way the engine assigns them as the document is updated, and therefore there is a conflict that should be reported as an error.

So the basic abstraction on this is that you have "two" update operations to perform, being one to "replace" the element where it exists, and the other to "push" the new element where it does not exist.

The best way to implement this is using "Bulk" operations, which whilst still "technically" is "two" update operations, it is however just a "single" request and response, no matter which condition was met:

var bulk = Model.collection.initializeOrderedBulkOp();

bulk.find({ "_id": id, "readers.user": req.user.id }).updateOne({
    "$set": { "readers.$.someData": data } }
});

bulk.find({ "_id": id, "readers.user": { "$ne": req.user.id } }).updateOne({
    "$push": { "readers": { "user": req.user.id, "someData": data } }
});

bulk.execute(function(err,result) {
    // deal with result here
});

If you really "need" the updated object in result, then this truly becomes a "possible" multiple request following the logic where the array element was not found:

Model.findOneAndUpdate(
    { "_id": id, "readers.user": req.user.id },
    { "$set": { "readers.$.someData": data } },
    { "new": true },
    function(err,doc) {
        if (err) // handle error;
        if (!doc) {
            Model.findOneAndUpdate(
                { "_id": id, "readers.user": { "$ne": req.user.id } },
                { "$push": { "readers":{ "user": req.user.id, "someData": data } } },
                { "new": true },
                function(err,doc) {
                    // or return here when the first did not match
                }
            );
        } else {
            // was updated on first try, respond
        }
    }
);

And again using you preferred method of not nesting callbacks with either something like async or nested promise results of some description, to avoid the basic indent creep that is inherrent to one action being dependant on the result of another.

Basically probably a lot more efficient to perform the updates in "Bulk" and then "fetch" the data afterwards if you really need it.


Complete Listing

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

mongoose.connect('mongodb://localhost/test');

var userSchema = new Schema({
  name: String
});

var dataSchema = new Schema({
  user: { type: Schema.Types.ObjectId, ref: 'User' },
  someData: String
},{ "_id": false });

var testSchema = new Schema({
  name: String,
  readers: [dataSchema]
});

var User = mongoose.model( 'User', userSchema ),
    Test = mongoose.model( 'Test', testSchema );

var userId = null,
    id = null;

async.series(

  [
    // Clean models
    function(callback) {
      async.each([User,Test],function(model,callback) {
        model.remove({},callback);
      },callback);
    },

    // Create a user
    function(callback) {
      User.create({ name: 'bill' },function(err,user) {
        userId = user._id;
        callback(err);
      });
    },

    function(callback) {
      Test.create({ name: 'Topic' },function(err,topic) {
        id = topic._id;
        console.log("initial state:");
        console.log(topic);
        callback(err);
      });
    },

    // 1st insert array 2nd update match 1 modified
    function(callback) {
      var bulk = Test.collection.initializeOrderedBulkOp();

      bulk.find({ "_id": id, "readers.user": userId }).updateOne({
        "$set": { "readers.$.someData": 1 }
      });

      bulk.find({ "_id": id, "readers.user": { "$ne": userId }}).updateOne({
        "$push": { "readers": { "user": userId, "someData": 1 } }
      });

      bulk.execute(function(err,result) {
        if (err) callback(err);
        console.log("update 1:");
        console.log(JSON.stringify( result, undefined, 2));
        Test.findById(id,function(err,doc) {
          console.log(doc);
          callback(err);
        });
      });
    },

    // 2nd replace array 1st update match 1 modified
    function(callback) {
      var bulk = Test.collection.initializeOrderedBulkOp();

      bulk.find({ "_id": id, "readers.user": userId }).updateOne({
        "$set": { "readers.$.someData": 2 }
      });

      bulk.find({ "_id": id, "readers.user": { "$ne": userId }}).updateOne({
        "$push": { "readers": { "user": userId, "someData": 2 } }
      });

      bulk.execute(function(err,result) {
        if (err) callback(err);
        console.log("update 2:");
        console.log(JSON.stringify( result, undefined, 2));
        Test.findById(id,function(err,doc) {
          console.log(doc);
          callback(err);
        });
      });
    },

    // clear array
    function(callback) {
      Test.findByIdAndUpdate(id,
        { "$pull": { "readers": {} } },
        { "new": true },
        function(err,doc) {
          console.log('cleared:');
          console.log(doc);
          callback(err);
        }
      );
    },

    // cascade 1 inner condition called on no array match
    function(callback) {
      console.log('update 3:');
      Test.findOneAndUpdate(
        { "_id": id, "readers.user": userId },
        { "$set": { "readers.$.someData": 1 } },
        { "new": true },
        function(err,doc) {
          if (err) callback(err);
          if (!doc) {
            console.log('went inner');
            Test.findOneAndUpdate(
              { "_id": id, "readers.user": { "$ne": userId } },
              { "$push": { "readers": { "user": userId, "someData": 1 } } },
              { "new": true },
              function(err,doc) {
                console.log(doc)
                callback(err);
              }
            );
          } else {
            console.log(doc);
            callback(err);
          }
        }
      );
    },

    // cascade 2 outer condition met on array match
    function(callback) {
      console.log('update 3:');
      Test.findOneAndUpdate(
        { "_id": id, "readers.user": userId },
        { "$set": { "readers.$.someData": 2 } },
        { "new": true },
        function(err,doc) {
          if (err) callback(err);
          if (!doc) {
            console.log('went inner');
            Test.findOneAndUpdate(
              { "_id": id, "readers.user": { "$ne": userId } },
              { "$push": { "readers": { "user": userId, "someData": 2 } } },
              { "new": true },
              function(err,doc) {
                console.log(doc)
                callback(err);
              }
            );
          } else {
            console.log(doc);
            callback(err);
          }
        }
      );
    }

  ],
  function(err) {
    if (err) throw err;
    mongoose.disconnect();
  }

);

Output:

initial state:
{ __v: 0,
  name: 'Topic',
  _id: 55f60adc1beeff6b0a175e98,
  readers: [] }
update 1:
{
  "ok": 1,
  "writeErrors": [],
  "writeConcernErrors": [],
  "insertedIds": [],
  "nInserted": 0,
  "nUpserted": 0,
  "nMatched": 1,
  "nModified": 1,
  "nRemoved": 0,
  "upserted": []
}
{ _id: 55f60adc1beeff6b0a175e98,
  name: 'Topic',
  __v: 0,
  readers: [ { user: 55f60adc1beeff6b0a175e97, someData: '1' } ] }
update 2:
{
  "ok": 1,
  "writeErrors": [],
  "writeConcernErrors": [],
  "insertedIds": [],
  "nInserted": 0,
  "nUpserted": 0,
  "nMatched": 1,
  "nModified": 1,
  "nRemoved": 0,
  "upserted": []
}
{ _id: 55f60adc1beeff6b0a175e98,
  name: 'Topic',
  __v: 0,
  readers: [ { user: 55f60adc1beeff6b0a175e97, someData: '2' } ] }
cleared:
{ _id: 55f60adc1beeff6b0a175e98,
  name: 'Topic',
  __v: 0,
  readers: [] }
update 3:
went inner
{ _id: 55f60adc1beeff6b0a175e98,
  name: 'Topic',
  __v: 0,
  readers: [ { someData: '1', user: 55f60adc1beeff6b0a175e97 } ] }
update 3:
{ _id: 55f60adc1beeff6b0a175e98,
  name: 'Topic',
  __v: 0,
  readers: [ { someData: '2', user: 55f60adc1beeff6b0a175e97 } ] }
Sign up to request clarification or add additional context in comments.

7 Comments

I tried the bulk solution when the readers array is empty, and appears that works, err variable is null, but, any element is added in the array of collection.
@AralRoca I have no idea what you are trying to say. The "two" updates basically are 1. Test where the array element is present and where so "update" the element data in place. 2. If the array element is not present then add a new element of the array. This is by no means the very first time I have implemented this code, and therefore if you have problems then you are not following the example "exactly". Read again an re-check the code you are using. Works for the rest of the world 100% of the time.
sorry @BlakesSeven, I don't know why, but I tried your bulk solution editing the Model and params and the err is null and result.isOk()is true, but nothing happen in the array in all cases... I debugged and all the params are correct.
@AralRoca Repeating myself. You are doing it wrong and not following the code as presented "exactly". It's very simple and the only problems will be in "your" transcription. Everything works exactly as designed.
Excuse me... but in first impression both solutions are wrong. The first solution have a bad token. The second solution have a if (err)condition that should be if (!err)... Despite this, the second solution (after condition edited) is working well. Using the same params, same transcription, the first solution (bulk solution) is not working for me.
|

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.