Home > Blockchain >  MongoDB push to nested array with findOneAndUpdate
MongoDB push to nested array with findOneAndUpdate

Time:04-09

I would like to know whether it is possible to implement the following problem within single call of findOneAndUdate() method. Lets 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 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 withing 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 solution online, read the docs but still can't find any similar problem that was successfully solved.

Most common similar problem people tend to hit is updating specific element in nested array under condition which can be solved by using 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 "logs" array but only for the last one. Since I already spent lot of time with this problem it leads me to the question whether it is even possible to implement or whether there are any limitations with current MongoDB version which would make such implementation impossible. Thanks for any help.

CodePudding user response:

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 $mergeArrays - 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

  • Related