1

I am trying to update a nested document in MongoDB that looks similar to below (shortened to be concise).

{
  "cols": "20",
  "saveTime": "2014-06-15-10:44:09",
  "rows": "20",
  "gameID" : "66f2497c-7a2b-4210-a06b-80be0e6a8fd8",
  "players": [
    {
      "id": "Inhuman",
      "num": "1",
      "color": "00ff00ff",
      "name": "badComputer",
      "type": "1"
    },
    <...>
  ],
  "nodes": [
    {
      "g": "1",
      "c": "0",
      "r": "0",
      "a": "0",
      "o": ""
    },
    {
      "g": "1",
      "c": "0",
      "r": "1",
      "a": "0",
      "o": ""
    }
    <...>
  ],
}

What I am trying to do is update one of the nodes. For example, I want to change the node:

{ "g": "1", "c": "0", "r": "0", "a": "0", "o": ""}

to

{ "g": "1", "c": "0", "r": "0", "a": "5", "o": ""}

I have tried using the dot (.) notation, with the $set command, ala:

db.game.update({"gameID" : "66f2497c-7a2b-4210-a06b-80be0e6a8fd8"}, { $set: {"nodes.r":"0",   "nodes.c":"0", "nodes.a":"5"}}),

But that does not give me the expected behavior because I'm updating all nodes with the same r and c values. This is obviously not what I want, but I do not see how to update a specific piece of this document. Does anyone have any idea how to do this?

Neil Lunn
  • 148,042
  • 36
  • 346
  • 317
unaligned
  • 160
  • 1
  • 9

2 Answers2

2

If you are looking to update a specific item in your "nodes" array that you do not know the position of but you know the "criteria" to match that item, then you need the $elemMatch operator along with the positional $ operator in the update side:

db.game.update(
    {
        "gameID" : "66f2497c-7a2b-4210-a06b-80be0e6a8fd8",
        "nodes": { "$elemMatch": { "g": 1, "r": 0 } } 
    },
    { "$set": { "nodes.$.c":"0", "nodes.$.a":"5" } }
)

The positional $ operator contains the matched "index" position of the first element "matched" by your query conditions. If you do not use $elemMatch and use the "dot notation" form instead, then the match is only valid for the whole document containing values that would be true and does not reflect the "position" of the element that matches both of the field conditions.

Care must be taken that this is the "first" match, and typically expected as the only match. The reason being that the positional operator will only contain the "first" position where there were multiple matches. To update more than one array item matching the criteria in this way, then you need to issue the update several times until the document is no longer modified.

For a "known" position you can always directly use the "dot notation" form, including the position of the element that you wish to update:

db.game.update(
    {
        "gameID" : "66f2497c-7a2b-4210-a06b-80be0e6a8fd8",
    },
    { "$set": { "nodes.0.c":"0", "nodes.0.a":"5" } }
)

So the index position is 0 for the first element, 1 for the second element and so on.

Noting that in both cases, you only need to pass to $set the fields you actually want to change. So unless you are unsure of a value being present ( which would not be the case if that was your query ) then you do not need to "set" fields to the value they already contain.

Neil Lunn
  • 148,042
  • 36
  • 346
  • 317
  • Thanks for the info, but one question. There will only ever be one element in the nodes array with given r and c (row and column) values. Would it still be necessary to do this, or would orepor's answer above work? – unaligned Jun 16 '14 at 05:07
  • @unaligned No it will not work, which is why I commented to them as well for their understanding. Say you were looking for `{ "g":,"r": 1 }`. Unless you use `$elemMatch` this does indeed match the "document" which has elements in the array that would match the condition. But his does not actually match the correct position of the array unless you use `$elemMatch`. The linked documentation shows other examples. But two properties will almost never match this way, except by chance. – Neil Lunn Jun 16 '14 at 05:17
0

To update specific node - you would need to put that in the query part of your search.

As in

  db.game.update({"gameID" : "66f2497c-7a2b-4210-a06b-80be0e6a8fd8","nodes.r":"0", 

  "nodes.c":"0", "nodes.a":"5" }, { $set: {"nodes.$.r":"0",   "nodes.$.c":"0", "nodes.$.a":"5"}})

You see the $ sign takes the node object it found that matches the first part (query) of the call, and sends you there in the second part (projection) part of your call. Also check out this question

Community
  • 1
  • 1
orepor
  • 905
  • 2
  • 13
  • 23
  • Actually this would only match on the provided data purely by chance that the element is the "first" item in the array. To correctly match with multiple criteria you need `$elemMatch` Since the document itself contains various array elements the condition as specified here matches the "document" itself and just returns the first position of the array. – Neil Lunn Jun 15 '14 at 22:50
  • yes you are right! This would actually not be correct – orepor Jun 16 '14 at 06:05