MongoDB - Push to nested array with findOneAndUpdate

497 views Asked by At

I would like to know whether it is possible to implement the following problem within a single call of findOneAndUpdate() method. Let's consider this collection entry:

[
  {
    "key": 1,
    "logs": [
      {
        "app_logs": [
          {
            "stdout": "app_stdout_1",
            "stderr": "app_stderr_1"
          }
        ],
        "syslog": "syslog_val1"
      },
      {
        "app_logs": [
          {
            "stdout": "app_stdout_2",
            "stderr": "app_stderr_2"
          }
        ],
        "syslog": "syslog_val2"
      }
    ]
  },
]

I need to push an additional element into app_logs array but only within the last entry of logs array. Currently to bypass this issue, I fetch the document as it is, execute such push in the application and then use findOneAndReplace() method. While this works I would like it to be implemented within a single MongoDB call. I have tried various approaches like pipeline aggregations, update array filters etc. but could not get any of them to work. Also tried to find the solution online, and read the docs but still can't find any similar problem that was successfully solved.

The most common similar problem people tend to hit is updating specific element in a nested array under conditions that can be solved by using a positional operator with something like:

findOneAndUpdate({
  "key": 1,
  "logs.syslog": "syslog_val1"
},
{
  "$push": {
    "logs.$.app_logs": {
      "stdout": "app_stdout_new",
      "stderr": "app_stderr_new"
    }
  }
})

In my case, I want to execute such push not for the specific element of the logs array but only for the last one. Since I already spent a lot of time with this problem it leads me to the question of whether it is even possible to implement or whether there are any limitations with the current MongoDB version which would make such implementation impossible. Thanks for any help.

1

There are 1 answers

1
Yong Shun On BEST ANSWER

For your scenario, via update query with Aggregation Pipeline to achieve it.

  1. $set - Get the last element of logs as lastLog.

  2. $set

    2.1 $concatArrays - Merge arrays with all the elements of logs (except the last document) and the result of 2.2.

    2.2 $mergeObjects - Merge objects with lastLog and the result of 2.3 aimed to override the app_logs field.

    2.3 $concatArrays - Merge arrays with lastLog.app_logs and new document (as array).

  3. $unset - Remove lastLog field.

db.collection.update({
  "key": 1,
  "logs.syslog": "syslog_val1"
},
[
  {
    $set: {
      "lastLog": {
        $arrayElemAt: [
          "$logs",
          -1
        ]
      }
    }
  },
  {
    $set: {
      "logs": {
        $concatArrays: [
          {
            $slice: [
              "$logs",
              {
                $add: [
                  {
                    $size: "$logs"
                  },
                  -1
                ]
              }
            ]
          },
          [
            {
              $mergeObjects: [
                "$lastLog",
                {
                  "app_logs": {
                    $concatArrays: [
                      "$lastLog.app_logs",
                      [
                        {
                          "stdout": "app_stdout_new",
                          "stderr": "app_stderr_new"
                        }
                      ]
                    ]
                  }
                }
              ]
            }
          ]
        ]
      }
    }
  },
  {
    $unset: "lastLogs"
  }
])

Sample Mongo Playground