Using $replaceOne on arrays

Hi community,

I know how to use $replaceOne on a singe field that is part of the target document:

{
  _id: 'my-document',
  uri: 'https://mydomain.com/images/funny.png'
}

Here, I can replace the uri like this:

{
      $set: {
        uri: {
          $replaceOne: {
            input: '$uri',
            find: 'https://mydomain.com',
            replacement: 'https://anotherdomain.com'
          }
        }
      }
}

This works just fine.

However, I also have documents that look like this:

 {
   _id: 'my-document',
   content: [
     {
       teaser: [{
           uri: 'mydomain.com/image/funny.png'
         }
       ]
     }
   ]
}

So the uri is two array levels down (both content and teaser are arrays).

How can I replace all of these uri fields, i.e. iterate over both arrays? Is this possible without using map? And if not, how would the map version look like?

Hello @waldgeist, Welcome to MongoDB Community Forum :slight_smile:

I don’t think is there any way without $map operator, you can use nested $map operator as below,

  • $map to iterate loop of content
  • $map to iterate loop of teaser
  • $replaceOne to replace uri same as you are doing for root field
[
  {
    $set: {
      content: {
        $map: {
          input: "$content",
          in: {
            teaser: {
              $map: {
                input: "$$this.teaser",
                in: {
                  $replaceOne: {
                    input: "$$this.uri",
                    find: "mydomain.com",
                    replacement: "anotherdomain.com"
                  }
                }
              }
            }
          }
        }
      }
    }
  }
]

Thanks for your fast answer, highly appreciated!

However, I forgot to mention that the objects in both content and teaser have further properties. So the shape of the document rather looks like this:

{
   _id: 'my-document',
   content: [
     {
       aContentProp: 'value',
       teaser: [{
           aTeaserProp: 'value',
           uri: 'mydomain.com/image/funny.png'
         }
       ]
     }
   ]
}

These properties get lost in the aggregations. Is there a way to preserve them? Ideally without explicitly naming these properties?

Another (minor) issue: If one of the keys does not exist, it will be set to null in the result set. Is there a way to prevent this?

  • You can use $mergeObjects operator to merge current object properties with updated property,
  • to prevent null value use $ifNull and $$REMOVE operator
[
  {
    $set: {
      content: {
        $map: {
          input: "$content",
          in: {
            $mergeObjects: [
              "$$this",
              {
                teaser: {
                  $map: {
                    input: "$$this.teaser",
                    in: {
                      $mergeObjects: [
                        "$$this",
                        {
                          uri: {
                            $ifNull: [
                              {
                                $replaceOne: {
                                  input: "$$this.uri",
                                  find: "mydomain.com",
                                  replacement: "anotherdomain.com"
                                }
                              },
                              "$$REMOVE"
                            ]
                          }
                        }
                      ]
                    }
                  }
                }
              }
            ]
          }
        }
      }
    }
  }
]

Wow, what an expression! Thank you so much for sharing this. That whole aggregation syntax is a mystery to me. I wish the docs would cover more of these “complex” cases than just the basics. So glad I found this forum.

1 Like

Hi @waldgeist,

Complex examples need some context on the problem to solve, so a lot of those will end up emerging via community discussion rather than being general recipes.

However, @Paul_Done, one of MongoDB’s most experienced Solution Architects, has recently released a free e-book covering Practical MongoDB Aggregations with great examples, tips, and cheat sheets:

This e-book is also linked from the Aggregation Reference documentation in the server manual, so hopefully that is a few steps closer to your wish :slight_smile: .

Regards,
Stennie

1 Like

Hi Stennie, thanks. I am totally aware of this. However, addressing and modifying objects inside arrays is something that should definitely be covered in the docs IMHO. I read though all the samples, but they only contain simple stuff like adding a value to all integers of an array. Also skimmed through Paul’s book yesterday, but couldn’t find examples for this either. But maybe I overlooked them.

Hello @waldgeist, here is another approach to update the nested array element field. This uses the $function aggregation operator. This operator allows define a custom JavaScript (JS) function (and use it within the aggregation). This requires MongoDB v4.4 or higher.

In this case use the JS function to iterate the outer and the inner arrays, and change the uri field value - but, using the JavaScript String#replace method (see String.replace at MDN).

The update with aggregation pipeline:

db.collection.updateOne(
{ },
[
{ 
  $set: {
      content: {
          $function: {
              body: function(content) {
                           for (let i = 0; i < content.length; i++) {
                               let tsr = content[i].teaser;
                               for (let j = 0; j < tsr.length; j++) {
                                   tsr[j].uri = tsr[j].uri.replace('mydomain', 'newdomain');
                               }
                               content[i].teaser = tsr;
                           }
                           return content;
              },
              args: [ "$content" ],
              lang: "js"
          }
      }
  }
}
] )

This is awesome. I was just about to ask if MongoDB has something like “stored procedures”, where you can do stuff like this programmatically.