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!

Community
  • 1
  • 1
Aral Roca
  • 5,442
  • 8
  • 47
  • 78

1 Answers1

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 } ] }
Blakes Seven
  • 49,422
  • 14
  • 129
  • 135
  • 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. – Aral Roca Sep 13 '15 at 12:57
  • @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. – Blakes Seven Sep 13 '15 at 13:31
  • 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. – Aral Roca Sep 13 '15 at 13:55
  • @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. – Blakes Seven Sep 13 '15 at 13:59
  • 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. – Aral Roca Sep 13 '15 at 20:54
  • @AralRoca Attached a complete self contained listing and it's output that you can run yourself that demonstrates both concepts and proves that they do indeed work. I suggest you read through it and run it as well so you understand it. – Blakes Seven Sep 13 '15 at 23:48
  • Thank you for everything! you're great. Finally was my fault. – Aral Roca Sep 14 '15 at 07:53