1

Suppose I have a document that looks like this:

{
    "id": 1,
    "entries": [
        {
            "id": 100,
            "urls": {
                "a": "url-a",
                "b": "url-b",
                "c": "url-c"
            },
            "revisions": []
        }
    ]
}

I am trying to add a new object to the revisions array that contains its own urls field. Two of the fields should be copied from the entry's urls, while the last one will be new. The result should look like this:

{
    "id": 1,
    "entries": [
        {
            "id": 100,
            "urls": {
                "a": "url-a",
                "b": "url-b",
                "c": "url-c"
            },
            "revisions": [
                {
                    "id": 1000,
                    "urls": {
                        "a": "url-a", <-- copied
                        "b": "url-b", <-- copied
                        "c": "some-new-url" <-- new
                    }
                }
            ]
        }
    ]
}

I am on MongoDB 4.2+, so I know I can use $property on the update query to reference values. However, this does not seem to be working as I expect:

collection.updateOne(
    {
        id: 1,
        "enntries.id": 100 
    },
    {
        $push: {
            "entries.$.revisions": {
                id: 1000,
                urls: {
                    "a": "$entries.$.urls.a",
                    "b": "$entries.$.urls.b",
                    "c": "some-new-url"
                }
            } 
        }       
    }
);

The element gets added to the array, but all I see for the url values is the literal $entries.$.urls.a. value I suspect the issue is with combining the reference with selecting a specific positional array element. I have also tried using $($entries.$.urls.a), with the same result.

How can I make this work?

2
  • You've few issues with query, So entires is an array, Do you want to do it on every object in entires array or for few or for only first one ? Commented May 19, 2020 at 19:22
  • I want to do it exactly for the one that matches the entries.id, so I believe what I have is correct. That part is behaving as I want. Commented May 19, 2020 at 19:26

1 Answer 1

3

Starting from MongoDB version >= 4.2 you can use aggregation pipeline in updates which means your update part of query will be wrapped in [] where you can take advantage of executing aggregation in query & also use existing field values in updates.

Issue :

Since you've not wrapped update part in [] to say it's an aggregation pipeline, .updateOne() is considering "$entries.$.urls.a" as a string. I believe you'll not be able to use $ positional operator in updates which use aggregation pipeline.

Try below query which uses aggregation pipeline :

collection.updateOne(
  {
    id: 1,
    "entries.id": 100 /** "entries.id" is optional but much needed to avoid execution of below aggregation for doc where `id :1` but no `"entries.id": 100` */,
  } 
  [
    {
      $set: {
        entries: {
          $map: { // aggregation operator `map` iterate over array & creates new array with values.
            input: "$entries",
            in: {
              $cond: [
                { $eq: ["$$this.id", 100] }, // `$$this` is current object in array iteration, if condition is true do below functionality for that object else return same object as is to array being created.
                {
                  $mergeObjects: [
                    "$$this",
                    {
                      revisions: { $concatArrays: [ "$$this.revisions", [{ id: 1000, urls: { a: "$$this.urls.a", b: "$$this.urls.b", c: "some-new-url" } } ]] }
                    }
                  ]
                },
                "$$this" // Returning same object as condition is not met.
              ]
            }
          }
        }
      }
    }
  ]
);

$mergeObjects will replace existing revisions field in $$this (current) object with value of { $concatArrays: [ "$$this.revisions", { id: 1000, urls: { a: "$$this.urls.a", b: "$$this.urls.b", c: "some-new-url" } } ] }.

From the above field name revisions and as it being an array I've assumed there will multiple objects in that field & So we're using $concatArrays operator to push new objects into revisions array of particular entires object.

In any case, if your revisions array field does only contain one object make it as an object instead of array Or you can keep it as an array & use below query - We've removed $concatArrays cause we don't need to merge new object to existing revisions array as we'll only have one object every-time.

collection.update(
  {
    id: 1,
    "entries.id": 100
  } 
  [
    {
      $set: {
        entries: {
          $map: {
            input: "$entries",
            in: {
              $cond: [
                { $eq: ["$$this.id", 100] },
                {
                  $mergeObjects: [
                    "$$this",
                    {
                      revisions:  [ { id: 1000, urls: { a: "$$this.urls.a", b: "$$this.urls.b", c: "some-new-url" } } ]
                    }
                  ]
                },
                "$$this"
              ]
            }
          }
        }
      }
    }
  ]
);

Test : Test your aggregation pipeline here : mongoplayground

Ref : .updateOne()

Note : If in any case .updateOne() throws in an error due to in-compatible client or shell, try this query with .update(). This execution of aggregation pipeline in updates helps to save multiple DB calls & can be much useful on arrays with less no.of elements.

Sign up to request clarification or add additional context in comments.

1 Comment

This is an amazingly complete answer, I appreciate you taking the time. Everything works flawlessly!

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.