Reputation: 5929
I'm using Mongoose (MongoDB in node.js), and after reading this answer:
I have another question:
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!
Upvotes: 3
Views: 12220
Reputation: 50426
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.
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 } ] }
Upvotes: 4