MongodDB $pull only one element from array [duplic

2019-01-25 13:13发布

问题:

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?

回答1:

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:

  1. find element that you want and that has userTags as foo
  2. iterate through userTags and remove one foo from it
  3. 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.



回答2:

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.



回答3:

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.