views:

74

answers:

2

Hi,

I'm a MongoDB newbie and wanted to ask how to write an update command involving upsert and list.

Basically I want to accomplish something like this:

{"_id" : ObjectId("4c28f62cbf8544c60506f11d"),
"some_other_data":"goes here",
"trips": [
    {"name": "2010-05-10",
     "loc": [{"lat":21.321231, "lng": 16.8783234, "updated_at": "Mon May 10 2010 15:24:35"}, 
        {"lat":21.321231, "lng": 16.8783234, "updated_at": "Mon May 10 2010 15:24:24"}]
    },
    {"name": "2010-05-08",
     "loc": [{"lat":21.324239, "lng": 16.8735234, "updated_at": "Mon May 8 2010 11:18:05"},
        {"lat":21.311234, "lng": 16.8743271, "updated_at": "Mon May 8 2010 11:17:55"}, 
        {"lat":21.321238, "lng": 16.8782219, "updated_at": "Mon May 8 2010 11:17:45"}]
    }
]}

Note that:

  • You supply a trip name and the current location
  • If the trip does not exist already, it needs to be created
  • trips.name should be unique so that if it exists, you append to the location array

This is the query I wrote combining the positional operator with $push.

    db.mycollection.update({"application_id": "MyTestApp", 
                            "trips.name": "2010-05-10"},
                           {$push: {'trips.$.loc': {"lat":11, "lng":11} }}, 
                           true);

But this results in data like this:

> db.mycollection.find({"application_id":"MyTestApp"})          
{ "_id" : ObjectId("4c29044ebf8544c60506f11f"), 
"application_id" : "MyTestApp", 
"trips" : { "$" : { "loc" : [ { "lat" : 11, "lng" : 11 } ] }, 
"name" : "2010-05-10" } 
}

You can see that

  • "trips" is not an array
  • it took "$" literally and created a key with that (doh!)

So far I've been pretty happy with MongoDB, but there's definitely a steep learning curve with writing complicated queries.

Any feedback will be appreciated.

Thanks in advance, Amie

+1  A: 

EDITED TO INCLUDE CORRECT SOLUTION

This is exactly the problem I hit learning Mongo - you're looking for the $addToSet operator (see docs here) that's used with the update command, in conjunction with the $ positional operator you were using.

$addToSet

{ $addToSet : { field : value } }

Adds value to the array only if its not in the array already.

The query thus becomes (db.stack is the collection I used for testing purposes), sample run to follow:

db.stack.update({ "trips.name":"2010-05-10" }, 
                { $addToSet: { "trips.$.loc":{"lat":11, "lng":12} } }
               );

TEST RUN (with some abbreviations for space of the elements that aren't important):

#### YOUR ITEM IN THE DB
> db.stack.find({"trips.name":"2010-05-10"})
{ "_id" : ObjectId("4c28f62cbf8544c60506f11d"), "some_other_data" : "goes here", 
    "trips" : [
    { "name" : "2010-05-10",
        "loc" : [ {
                "lat" : 21.321231,
                "lng" : 16.8783234,
                "updated_at" : "Mon May 10 2010 15:24:35"
            }, { "lat" : 21.321231,
                "lng" : 16.8783234,
                "updated_at" : "Mon May 10 2010 15:24:24"
            } ] },
    { "name" : "2010-05-08",
        "loc" : [ ... ]
    } ] }
#### SUCCESSFULLY ADDS ITEM TO PROPER ARRAY
> db.stack.update({"trips.name":"2010-05-10"}, {$addToSet: {"trips.$.loc":{"lat":11, "lng":11}}});
> db.stack.findOne()
{ "_id" : ObjectId("4c28f62cbf8544c60506f11d"), "some_other_data" : "goes here",
    "trips" : [
        { "loc" : [
                { "lat" : 21.321231,
                    "lng" : 16.8783234,
                    "updated_at" : "Mon May 10 2010 15:24:35"
                }, { "lat" : 21.321231,
                    "lng" : 16.8783234,
                    "updated_at" : "Mon May 10 2010 15:24:24"
                }, { "lat" : 11,
                    "lng" : 11
                }
            ], "name" : "2010-05-10"
        },
        { "name" : "2010-05-08",
            "loc" : [ ...  ]
        } ] }
#### ON REPEAT RUN DOESN'T ADD NEW ELEMENT
> db.stack.update({"trips.name":"2010-05-10"}, {$addToSet: {"trips.$.loc":{"lat":11, "lng":11}}});
> db.stack.findOne()
{ "_id" : ObjectId("4c28f62cbf8544c60506f11d"), "some_other_data" : "goes here",
    "trips" : [ {
            "loc" : [
                { "lat" : 21.321231,
                    "lng" : 16.8783234,
                    "updated_at" : "Mon May 10 2010 15:24:35"
                }, { "lat" : 21.321231,
                    "lng" : 16.8783234,
                    "updated_at" : "Mon May 10 2010 15:24:24"
                }, { "lat" : 11,
                    "lng" : 11
                }
            ], "name" : "2010-05-10"
        },
        { "name" : "2010-05-08",
            "loc" : [ ...  ]
        } ] }
#### BUT WILL CORRECTLY ADD ANOTHER ELEMENT TO THE SAME ARRAY IF IT'S NOT PRESENT
> db.stack.update({"trips.name":"2010-05-10"}, {$addToSet: {"trips.$.loc":{"lat":11, "lng":12}}});
> db.stack.findOne()
{ "_id" : ObjectId("4c28f62cbf8544c60506f11d"), "some_other_data" : "goes here",
    "trips" : [
        { "loc" : [
                { "lat" : 21.321231,
                    "lng" : 16.8783234,
                    "updated_at" : "Mon May 10 2010 15:24:35"
                }, { "lat" : 21.321231,
                    "lng" : 16.8783234,
                    "updated_at" : "Mon May 10 2010 15:24:24"
                }, { "lat" : 11,
                    "lng" : 11
                }, { "lat" : 11,
                    "lng" : 12
                }
            ], "name" : "2010-05-10"
        },
        { "name" : "2010-05-08",
            "loc" : [ ... ]
    } ] }
nearlymonolith
Thanks for the response @Anthony Morelli. I tried $addToSet and had to pass in upsert=true to insert a trip for the first time. When I changed the trip name to something else, this actually creates a new document instead of using the current doc. How can we specify that "trips" is an array?
Grnbeagle
What query did you run specifically? $addToSet only works with arrays, so I don't think that specification is the problem.
nearlymonolith
I ran your query exactly, and it didn't create a doc, so I passed in upsert=true. After it creates the first doc, when it ran for the second time, it created a second doc instead of appending it to the previous one..
Grnbeagle
I believe I fixed it - see edit. Basically, I forgot the positional operator which is necessary to specify which item in the trips array should be updated. The new query is `db.stack.update({"trips.name":"2010-05-10"}, {$addToSet: {"trips.$.loc":{"lat":11, "lng":12}}});` - I tested it with your existing schema and it works fine.
nearlymonolith
Is it correct to say that this only works if the document already exists? (As Scott pointed out, we can't use "$" and an upsert together) But I can see this can be useful in some cases. Thanks for the comprehensive update!
Grnbeagle
Unfortunately, it would only work on a preexisting doc. But you could write a query to test and then either insert or update that would process just about as quickly. A find operation would get you the ObjectID (if existing), so the find and then update would be optimized as looking up a specific ObjectID would add a negligible overhead. In the event that the document didn't exist, be able to insert it (again with minimal added overhead). Is that what you're looking for?
nearlymonolith
+1  A: 

You can't mix the positional operator ("$") and an upsert; the "$" will be treated as a field name during the insert. You can't do this for new documents, only existing one.

I suggested a structure more like this:

{"_id" : ObjectId("4c28f62cbf8544c60506f11d"),
"some_other_data":"goes here",
"trips": { 
    "2010-05-10":
       [{"lat":21.321231, "lng": 16.8783234, "updated_at": "Mon May 10 2010 15:24:35"}, 
        {"lat":21.321231, "lng": 16.8783234, "updated_at": "Mon May 10 2010 15:24:24"}],
    "2010-05-08": 
       [{"lat":21.324239, "lng": 16.8735234, "updated_at": "Mon May 8 2010 11:18:05"},
        {"lat":21.311234, "lng": 16.8743271, "updated_at": "Mon May 8 2010 11:17:55"}, 
        {"lat":21.321238, "lng": 16.8782219, "updated_at": "Mon May 8 2010 11:17:45"}]
    }
}

Then you can issue an update like this:

db.mycollection.update({application_id: "MyTestApp", "trips.2010-05-10":{$exists:true}},
                       {$push: {"trips.2010-05-10": {lat:11, lng:11} }}, 
                       true);

results in this being inserted.

> db.mycollection.find()
{ "_id" : ObjectId("4c2931d17b210000000045f0"), 
    "application_id" : "MyTestApp", 
    "trips" : { "2010-05-10" : [ { "lat" : 11, "lng" : 11 } ] } }

and running it again give you this:

> db.mycollection.find()
{ "_id" : ObjectId("4c2932db7b210000000045f2"), 
    "application_id" : "MyTestApp", 
    "trips" : { "2010-05-10" : 
        [ { "lat" : 11, "lng" : 11 }, 
          { "lat" : 11, "lng" : 11 } ] } }
Scott Hernandez
This is very close to what I tried to achieve. Thanks @Scott Hernandez. With this schema, is it possible to retrieve a list of keys in the "trips"? ("2010-050-10", etc) db.mycollection.find({"application_id":"MytestApp"}, {"trips":true}) returns the entire "trips" hash.
Grnbeagle
I ended up going with this method. Thanks for the solution Scott!
Grnbeagle