Meteor Collection#Update Behaviour


#1

Hi,
I have a question about Collection update, here an example

I declare two objects

var obj1 = {a:1, b:1, c:{d:1} }
var obj2 = {a:2, b:2, c:{d:2}, e:2}

I insert the first object in a collection
myCollection.insert(obj1)

If i fetch the doc (_id = ID) i have obviously the exact object obj1:
myCollection.find({_id:ID})
–>
{a:1, b:1, c:{d:1} }

Now i update the doc with obj2

myCollection.update({_id: ID}, {$set:obj2}})
myCollection.find({_id:ID})
–>
{a:2, b:2, c:{d:2}, e:2}

Everything works as expected.

I do a redundant step here
myCollection.update({_id: ID, {$set:obj1}})

myCollection.find({_id:ID})
–>
{a:1, b:1, c:{d:1}, e:2 }

and that’s fine.

Now, i define a third obj

var subObj3 = {f:3}

and i try to update the key c of the doc, which contains an object ({d:1})

myCollection.update({_id: ID}, { $set : { c: subObj3 }});

I have
myCollection.find({_id:ID})
–>
{a:1, b:1, c:{f:3}, e:2 }

Instead i would expect
{a:1, b:1, c:{d:1, f:3}, e:2 }

is this the correct behaviour?

If so, what would be the best pattern to update a subobject like in this case?

Let’s assume i have an object like

var subObj4 = {g:4,h:4,i:4}

How can i update the key ‘c’ of the doc in the collection keeping the previous unmatched values if any (in this example it would be {f:3} ) ?

Thanks for your help.


#2

This comes down to the mongo operator ‘$set’. If you use ‘$set’, it replaces the object in full. So the behavior you are seeing is expected.

To do what you are describing, you have many options, but here are two:

  • Set the subdocument directly. {$set: {“c.f”:subObj3}}
  • A more flexible option, set your ‘c’ object up as an array, and make use of
    the $addToSet mongo operator which only adds an object to an array if
    it is not already present.

There are many other methods, and it really depends on the use case and what your write and reads are likely to look like.


#3

Thanks for replying,

the first solution solution as you say is not very flexible, i can only update a single property of the subObject

i don’t think the second other solution suits my needs neither, my subObject does not seem to be modeled by an array.

Here my case in more details:

In my users collection i have the ‘profile’ key which contains an object

{
"_id" : “hzmiriSYenMvpn7i3”,
“createdAt” : ISODate(“2015-04-14T16:42:35.465+02:00”),
“services” : {
//SKIP …
},
“username” : “giggio”,
“emails” : [
//SKIP …
],
“profile” : {
“language” : “en”,
“currency” : “BRL”,
“roundings” : “0.1”,
“ask” : “0”,
“isNotifyOnAddExp” : true,
“isNotifyOnReport” : true,
“img” : 3,
“fbid” : null
}
}

Sometimes i only pass to the server a subset of this object to update and i want to keep the other unmatched values,

for example i pass

var updatingObj = {language: ‘it’, version: 1}

please note that language is an existing property and version is a new one.

My approach would be the following:

  • Fetching with a findOne the current user profile
  • extending it with the updatingObj ( something like var newProfile = _.extend(user.profile, updatingObj) )
  • updating the doc with $set : {profile : newProfile}

The drawback is that i need to do a findOne for each update… What do you think?


#4

In that case, there is a very straightforward solution:

Meteor.users.update( { _id: userId } , { $set:{"profile.language":"it" , "profile.version": 1}});

$set will create a field/subdocument if it does not exist.


#5

updatingObj can contain different mix of properties , i don’t have a fixed pattern

for example

var updatingObj = {language: ‘it’, version: 1}
var updatingObj = {language: ‘it’, roundings: 2}
var updatingObj = {ask: 1, img: 5}


#6

This is a fun one. Seemingly simple questions can become pretty complex. Your solution would work fine and only has the drawback you mentioned, I would probably go with that for now.

To do this without the fetch and in one operation, the only thing that I’m aware of that might suit this is a use of the findAndModify operator. I do not believe this is supported by Meteor at the moment (i could be wrong).

Any other solution I can come up with requires multiple update hits to the db, and adds quite a bit to the complexity. This is likely an infrequent operation, and not very database intensive, so I doubt it matters one way or the other, but the most streamlined solution I can come up with to answer what you were originally seeking to do is :

Create a profile configuration array

Meteor.users.update({_id: Meteor.userId() }, { $set: { "profile.config" :  [ ]  });

And then to add any additional features I would create an array such as:

var updateObj = [{ opt:"version", val: 1 } , { opt:"language", val:"it" },...]

And the update operations looks like this:

var updateOpts = _.pluck( updateObj , 'opt' );

/* $addToSet looks at complete objects in the array, we must pull out config opts that exist and we are changing their value or $addToSet will simply add another object to the array with the same opt, but the new value */

Meteor.users.update({_id: Meteor.userId() },{$pull:{ "profile.config":{opt:{$in: updateOpts }}}});

/*Then add the new opts to the array */

Meteor.users.update({_id: Meteor.userId()},{$addToSet:{ "profile.config": {$each:updateObj } }});

I have not tested this, but it should work