0

I'd like to select the first item from a nested Array, without fetching the whole document.

Schema/Model

Suppose I have a Schema like so:

const parentSchema = mongoose.Schema({
  name: String,
  children: []
});

const grandparentSchema = mongoose.Schema({
  name: String,
  children: [parentSchema]
})

Which would translate to this example instance:

{
  name: 'Grandparent Foo',
  children: [
    {
      name: 'Parent Foo',
      children: ['Child Foo', 'Child Bar', 'Child Baz']
    }
  ]
}

Question

I would like to get the first child of 'Parent Foo', so to boil it down I should be getting back 'Child Foo'

Notes

  • As you can see, the grandchildren are plain Strings, not Documents themselves (in contrast with the Parent) so I can't select them using dot notation.

  • I don't want to return the whole document and filter through it in code. I'd like to get over the wire only the first grandchild since the grandchildren Array (the children array of 'Parent Foo') can potentially contain millions of entries.

  • I need this because I want to $pop the first grandchild and return it. To do that, I plan on fetching the item first and then $pop it off, hence why I ask this question

1 Answer 1

1

You cannot really, without throwing extra work at the database.

As a general explanation:

Grandparent.find(
  { "children.name": "Parent Foo" },
  { "children.$": 1 }
)

Will return just the matched entry from "children" and no others should they exist.

If you explicitly need the "first" array element, then you use .aggregate():

Granparent.aggregate([
  { "$match": { "children.name": "Parent Foo" } },
  { "$addFields": {
    "children": {
      "$map": {
        "input": {
          "$filter": {
            "input": "$children",
            "as": "child",
            "cond": { "$eq": [ "$$child.name", "Parent Foo" ] }
          }
        },
        "as": "child",
        "in": {
          "name": "$$child.name",
          "children": { "$arrayElemAt": [ "$$child.children", 0 ] }
        }
      }
    }
  }}
])

So there you basically use $filter to replicate the standard positional match an then use $map to reshape with $arrayElemAt or $slice to actually get the first element of the inner array.

By contrast, if you live with returning "a small amount of extra data", then you just slice off of the positional match:

Grandparent.find(
  { "children.name": "Parent Foo" },
  { "children.$": 1 }
).lean().exec((err,docs) => {
  docs = docs.map( doc => {
    doc.children = doc.children.map( c => c.children = c.children.slice(0,1) );
    return doc;
  });
  // do something with docs

So we returned a little more in the cursor and just got rid of that very little bit of data with minimal effort.

Mileage may vary on this due to the actual size of real data, but if the difference is "small", then it's usually best to "trim" in the client rather than the server.

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

2 Comments

Grand - Apologies for not tagging this with mongoose. I'm guessing this is a standard mongo driver answer hence the db.collection.find instead of Grandparent.find(), is that correct? I'd like to test drive the solutions you gave me here but I'm not sure it's the same env.
@NicholasKyriakides Yep. I would quite often actually remove such a tag, since there is absolutely nothing mongoose "specific" in the question anyway. Use the model name, but everything else stays the same. Except using a callback or promise of course.

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.