Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

MongoDB conditionally $addToSet sub-document in array by specific field

Is there a way to conditionally $addToSet based on a specific key field in a subdocument on an array?

Here's an example of what I mean - given the collection produced by the following sample bootstrap;

cls
db.so.remove();
db.so.insert({
    "Name": "fruitBowl",
    "pfms" : [
        {
            "n" : "apples"
        }
    ]
});

n defines a unique document key. I only want one entry with the same n value in the array at any one time. So I want to be able to update the pfms array using n so that I end up with just this;

{
    "Name": "fruitBowl",
    "pfms" : [
        {
            "n" : "apples",
            "mState": 1111234
        }
    ]
}

Here's where I am at the moment;

db.so.update({
  "Name": "fruitBowl",
},{
// not allowed to do this of course
//    "$pull": {
//  "pfms": { n: "apples" },
//    },

"$addToSet": {
  "pfms": {
    "$each": [
      {
        "n": "apples",
        "mState": 1111234
      }
    ]
   }
  }
 }
)

Unfortunately, this adds another array element;

db.so.find().toArray();

[
    {
        "Name" : "fruitBowl",
        "_id" : ObjectId("53ecfef5baca2b1079b0f97c"),
        "pfms" : [
            {
                "n" : "apples"
            },
            {
                "n" : "apples",
                "mState" : 1111234
            }
        ]
    }
]

I need to effectively upsert the apples document matching on n as the unique identifier and just set mState whether or not an entry already exists. It's a shame I can't do a $pull and $addToSet in the same document (I tried).

What I really need here is dictionary semantics, but that's not an option right now, nor is breaking out the document - can anyone come up with another way?

FWIW - the existing format is a result of language/driver serialization, I didn't choose it exactly.

further

I've gotten a little further in the case where I know the array element already exists I can do this;

db.so.update({
    "Name": "fruitBowl",
    "pfms.n": "apples",
},{
  $set: {
   "pfms.$.mState": 1111234,
  },
 }
)

But of course that only works;

  1. for a single array element
  2. as long as I know it exists

The first limitation isn't a disaster, but if I can't effectively upsert or combine $addToSet with the previous $set (which of course I can't) then it the only workarounds I can think of for now mean two DB round-trips.

like image 852
cirrus Avatar asked Aug 14 '14 18:08

cirrus


People also ask

How do you update a field in an array in MongoDB?

You can use the updateOne() or updateMany() methods to add, update, or remove array elements based on the specified criteria. It is recommended to use the updateMany() method to update multiple arrays in a collection.

How do I remove an element from an array in MongoDB?

To remove an element, update, and use $pull in MongoDB. The $pull operator removes from an existing array all instances of a value or values that match a specified condition.

What is addToSet in MongoDB?

The $addToSet operator adds a value to an array unless the value is already present, in which case $addToSet does nothing to that array. The $addToSet operator has the form: { $addToSet: { <field1>: <value1>, ... } } To specify a <field> in an embedded document or in an array, use dot notation.


1 Answers

I have faced the exact same scenario. I was inserting and removing likes from a post.

What I did is, using mongoose findOneAndUpdate function (which is similar to update or findAndModify function in mongodb).

The key concept is

  • Insert when the field is not present
  • Delete when the field is present

The insert is

findOneAndUpdate({ _id: theId, 'likes.userId': { $ne: theUserId }},
{ $push: { likes: { userId: theUserId, createdAt: new Date() }}},
{ 'new': true }, function(err, post) { // do the needful }); 

The delete is

findOneAndUpdate({ _id: theId, 'likes.userId': theUserId},
{ $pull: { likes: { userId: theUserId }}},
{ 'new': true }, function(err, post) { // do the needful }); 

This makes the whole operation atomic and there are no duplicates with respect to the userId field.

I hope this helpes. If you have any query, feel free to ask.

like image 71
Suhel Chakraborty Avatar answered Sep 21 '22 11:09

Suhel Chakraborty