This question already has an answer here:
-
How to pull one instance of an item in an array in MongoDB?
1 answer
I have a document with an array inside, like this:
"userTags" : [
"foo",
"foo",
"foo",
"foo",
"moo",
"bar"
]
If I perform db.products.update({criteriaToGetDocument}, {$push: {userTags: "foo"}}}
I can correctly insert another instance of foo
into the array.
However, if I do db.products.update({criteriaToGetDocument}, {$pull: {userTags: "foo"}}}
then it removes all instances of foo
from the array, leaving me with:
"userTags" : [
"moo",
"bar"
]
This won't do at all, I only want to pull one item from the array rather than all of them. How can I alter the command so that only one foo
is removed? Is there some sort of $pullOnce
method that can work here?
No, there is nothing like this at the moment. A lot of people already requested the feature and you can track it in mongodb Jira. As far as you can see it is not resolved and also not scheduled (which means you have no luck in the near future).
The only option is to use application logic to achieve this would be:
- find element that you want and that has userTags as foo
- iterate through userTags and remove one foo from it
- update that element with a new userTags
Keep in mind that this operation breaks atomicity, but because Mongo has not provided a native method to do so, you will break atomicity in any way.
I moved one alternative solution to the new answer, because it does not answer this question, but represents one of the approaches to refactor existing schema. It also became so big, that started to be much bigger then the original answer.
If you really want to use this feature, you have to consider modifying your schema. One way to go is to do something like this:
"userTags" : {
"foo" : 4,
"moo": 1,
"bar": 1
}
Where the number is the quantity of tags. This way you can use atomic $inc to add or remove the tag. While adding the new tag can be appropriate, while deleting the tags you have to be careful. You have to check that the tag exist and that it is >= then 1.
Here is how you can do it:
db.a.insert({
_id : 1,
"userTags" : {
"foo" : 4,
"moo": 1,
"bar": 1
}
})
To add one Tag you need to do just this:
db.a.update({_id : 1}, {$inc : {'userTags.zoo' : 1}})
If the tag did not existed before it will create it
To remove the Tag, you need to do a little bit more:
db.a.update({
_id : 1,
'userTags.moo' : {$gte : 1 }
}, {
$inc : {'userTags.moo' : -1}
})
Here you are checking that the element exist and is bigger then 1 (no need to check for existence because $gte is doing this as well).
Keep in mind that this approach has one drawback. You can have some tags that are listed as fields, but are not real tags (they have a value of 0). So if you want to find all elements with tag foo you have to remember this and do something like this:
db.a.find({'userTags.zoo' : { $gte : 1}})
Also when you output all tags for an element, you might have the same problem. So you need to sanitize the output on application layer.
I know it's a workaround and not a real solution but, as I ran into this same issue, I found that in PHP at least, you can do something like this:
// Remove up to two elements "bar" from an array called "foo"
// in a document that matches the query {foo:"bar"}
$un_bar_qty = 2;
$un_bar_me = $db->foo->findOne(array("foo"=>"bar"));
foreach($un_bar_me['bar'] as $foo => $bar) {
if($bar == 'bar') {
unset($un_bar_me['bar'][$foo]);
$un_bar_qty--;
}
if(!$un_bar_qty) break;
}
$db->foo->update(array('_id'=>$un_bar_me['_id']),$bar);
Document before:
{
{ foo: "bar" },
{ bar: [ "bar" , "bar" , "foo", "bar" ] }
}
Document after:
{
{ foo: "bar" },
{ baz: [ "foo", "bar" ] }
}
Not too bad of a way to do it. Certainly seemed less of a hassle to me than messing around with tags and increments, YMMV.