Jason Cust
Jason Cust

Reputation: 10919

Is It possible to use query projection on the same collection that has a $elemMatch projection?

I understand that you can limit the items in a subcollection array using $elemMatch as a projection. When using it as such it returns all fields of the subdocuments that match regardless if query projections are also specified.

Is it possible to limit the fields returned in the matching subdocuments? How would you do so?

Using version 3.8.9.

Given the simple schemas:

var CommentSchema = mongoose.Schema({
  body: String,
  created: {
    by: {
      type: String,
      required: true
    },
    date: {
      type: Date,
      default: Date.now
    }
  }
});

var BlogSchema = mongoose.Schema({
  title: String,
  blog: String,
  created: {
    by: {
      type: String,
      required: true
    },
    date: {
      type: Date,
      default: Date.now
    }
  },
  comments: [CommentSchema]
});

var Blog = mongoose.model('Blog',modelSchema);

Example

Blog.findOne({_id: id}, {_id: 1, comments: {$elemMatch: {'created.by': 'Jane'}, body: 1}}, function(err, blog) {
  console.log(blog.toJSON());
});
// outputs:
{
  _id: 532cb63e25e4ad524ba17102,
  comments: [
    _id: 53757456d9c99f00009cdb5b,
    body: 'My awesome comment',
    created: { by: 'Jane', date: Fri Mar 21 2014 20:34:45 GMT-0400 (EDT) }
  ]
}

// DESIRED OUTPUT
{
  _id: 532cb63e25e4ad524ba17102,
  comments: [
    body: 'My awesome comment'
  ]
}

Upvotes: 0

Views: 484

Answers (1)

Neil Lunn
Neil Lunn

Reputation: 151230

Yes there are two ways to do this. So you can either use the $elemMatch on the projection side as you already have, with slight changes:

Model.findById(id,
   { "comments": { "$elemMatch": {"created.by": "Jane" } } },
   function(err,doc) {

Or just add to the query portion and use the positional $ operator:

Model.findOne(
    { "_id": id, "comments.created.by": "Jane" },
    { "comments.$": 1 },
    function(err,doc) {

Either way is perfectly valid.

If you wanted something a little more involved than that, you can use the .aggregate() method and it's $project operator instead:

Model.aggregate([
    // Still match the document
    { "$match": "_id": id, "comments.created.by": "Jane" },

    // Unwind the array
    { "$unwind": "$comments" },

    // Only match elements, there can be more than 1
    { "$match": "_id": id, "comments.created.by": "Jane" },

    // Project only what you want
    { "$project": {
        "comments": {
            "body": "$comments.body",
            "by": "$comments.created.by"
        }
    }},

    // Group back each document with the array if you want to
    { "$group": {
        "_id": "$_id",
        "comments": { "$push": "$comments" }
    }}
],
function(err,result) {

So the aggregation framework can be used for a lot more than simply aggregating results. It's $project operator gives you more flexibility than is available to projection using .find(). It also allows you to filter and return multiple array results, which is also something that cannot be done with projection in .find().

Upvotes: 1

Related Questions