How to manage array of documents nested within an object within an array

I want to manage (append, remove, and update) documents within an array, and the array field is within a document within an array. I’m talking about the “notifications” array visible in the following sample document:

{
  docId: "123",
  outcomes: [
    {
      prizeId: "test",
      notifications: [
        {
          action: "foo",
          foreignKey: "abc"
        }
      ]
    }
  ]
}

Given these parameters provided by a client,

params = {
  docId: "123",
  prizeId: "test",
  action: "foo",
}

I am able to select (find) the document with the following filter:

filter = {docId: params.docId, outcomes: {$elemMatch: {prizeId: params.prizeId}}}
db.getCollection('collection-name').find(filter)

I am not able to modify the document. For “remove”, I tried the following:

update = {
  $pull: {
    "outcomes.$.notifications": {
      $elemMatch: {
        action: params.action
      }
    }
  }
}
db.getCollection('collection-name').updateOne(filter, update)

As well as

update = {
  $pull: {
    "outcomes.$[outcome].notifications": {
      $elemMatch: {
        action: params.action
      }
    }
  }
}

options = {
  arrayFilters: [
    { "outcome.prizeId" : params.prizeId },
    // { "notification.action" : action },
  ]
}
db.getCollection('collection-name').updateOne(filter, update, options)

I’m suprised I cannot find examples in the documentation that deal with removing an array item from an array nested within a document within an array.

UPDATE: Actually I did find an example showing how to remove an item from a nested array. Unfortunately, I believe I am doing the same technique for my data, but the results are not the same. The main difference I can see is that I’m using updateOne and the example uses updateMany.

UPDATE 2: I believe the example does not do what I need. It updates multiple documents, and potentially multiple (outer) array items, and the syntax I need should constrain to updating one (outer) array item.

hi @John_Grant1

If I understand correctly, this input:

params = {
  docId: "123",
  prizeId: "test",
  action: "foo",
}

means “Find docId=123, prizeId=test, action=foo and remove it from the document”. Is this correct?

I think this is a difficult situation to solve, since in my mind, the update you need is to match docId=123 and prizeId=test, but you want the resulting document to not contain action=foo. The schema that contains array inside array is also difficult to work with, as you have observed.

Instead, I would suggest you to explore alternative schema design. I think if you remove one array layer would make things much easier. For example, if this is the original schema:

{
    docId: '123',
    outcomes: [
        {
            prizeId: 'test',
            notifications: [
                { action: 'foo', foreignKey: 'abc' },
                { action: 'bar', foreignKey: 'def' }
            ]
        },
        {
            prizeId: 'test2',
            notifications: [
                { action: 'foo', foreignKey: 'abc' },
                { action: 'aaa', foreignKey: 'ccc' },
            ]
        }
    ]
}

How about spreading this across two documents instead by unwinding the outcomes array:

[
    {
      docId: '123',
      outcomes: {
        prizeId: 'test',
        notifications: [
          { action: 'foo', foreignKey: 'abc' },
          { action: 'bar', foreignKey: 'def' }
        ]
      }
    },
    {
      docId: '123',
      outcomes: {
        prizeId: 'test2',
        notifications: [
          { action: 'foo', foreignKey: 'abc' },
          { action: 'aaa', foreignKey: 'ccc' }
        ]
      }
    }
]

Then the update becomes:

db.test.updateOne(
    {docId:'123', 'outcomes.prizeId': 'test'},
    [
        {$addFields: {
            'outcomes.notifications': {
                $filter: {
                    input: '$outcomes.notifications',
                    cond: {$ne: ['$$this.action', 'foo']}
                }
            }
        }}
    ]
)

Note that I’m using an aggregation pipeline to perform the update.

Result is:

[
  {
    _id: ObjectId("6409236401dd5b18bd5800f3"),
    docId: '123',
    outcomes: {
      prizeId: 'test',
      notifications: [ 
        { action: 'bar', foreignKey: 'def' }
      ]
    }
  },
  {
    _id: ObjectId("6409236401dd5b18bd5800f4"),
    docId: '123',
    outcomes: {
      prizeId: 'test2',
      notifications: [
        { action: 'foo', foreignKey: 'abc' },
        { action: 'aaa', foreignKey: 'ccc' }
      ]
    }
  }
]

Of course this may not work for your use case, but I think this is much easier to work with by avoiding array-inside-array complexity.

Best regards
Kevin

Hi @kevinadi ,

Yes, your understanding is correct. The resulting document should not contain action=foo FOR prizeId=test. The example document you provided is a good one for discussion. prizeId=test2 should not be affected by the update for the input “params”.

I agree, my schema is challenging to work with. I am intrigued by the suggestion to spread an array across multiple documents. I might look into that.

Another trick I have used in the past is to have 2 arrays at the same level (siblings) within the sole document, and I make sure to keep them in sync with each other. This is my first choice if I cannot find a mechanism to remove an item from a nested array.

Can you confirm if an item can be removed from a single nested array? The reference I found makes it seem like the removal will impact multiple outer array items.

I am assuming I should use $pull for an implementation to remove an item from an array. If there is a different operator I should consider, please let me know.

Thank you.
John

This sounds like a different layer of complexity, where you need to ensure things are in sync all the time. I’m not sure if this is better or worse than trying to work with array-inside-array :slight_smile:

Yes you’re correct that given an array-inside-array, the $pull operation will affect multiple array items, and so far I haven’t found a good workaround for this. In my mind, if it requires workarounds, then perhaps it’s not the best way forward.

I would definitely consider remodeling the schema though. Not having to fight the query language or the schema to do your work seems quite appealing to me :slight_smile:

Best regards
Kevin