akbr
akbr

Reputation: 105

Understanding overlapping mongodb projections

I'm struggling to understand how overlapping projections work in mongodb.

Here's a quick example to illustrating my conundrum (results from the in-browser mongodb console).

First, I created and inserted a simple document:

var doc = {
  id:"universe", 
  systems: {
    1: {
      name:"milky_way",
      coords:{x:1, y:1}
    }
  }
}

db.test.insert(doc);
// doc succesfully inserted

Next, I try a somewhat odd projection:

db.test.find({}, {"systems.1.coords":1, "systems.1":1});

//result
"_id" : ObjectId("537fd3541cdcaf1ba735becb"),
    "systems" : {
        "1" : {
            "coords" : {
                "y" : 1,
                "x" : 1
            }
        }
    }
}

I expected to see the entirety of "system 1," including the name field. But it appears the deeper path to "systems.1.coords" overwrote the shallower path to just "system.1"?

I decide to test this "deeper path overrides shallower path" theory:

db.test.find({}, {"systems.1.coords.x":1, "systems.1.coords":1});

//result
"_id" : ObjectId("537fd3541cdcaf1ba735becb"),
    "systems" : {
        "1" : {
            "coords" : {
                "y" : 1, // how'd this get here, but for the shallower projection?
                "x" : 1
            }
        }
    }
}

Here, my deeper projection didn't override the shallower one.

What gives? How is mongodb dealing with overlapping projections? I can't find the logic to it.

EDIT:

My confusion was stemming from what counted as a "top level" path.

This worked like I expected: .findOne({}, {"systems.1":1, "systems":1}) (i.e., a full set of systems is returned, notwithstanding that I started with what appeared to be a "narrower" projection).

However, this did not work like I expected: .findOne({}, {"systems.1.name":1, "systems.1":1}) (i.e., only the name field of system.1 is returned).

In short, going more than "one dot" deep leads to the overwriting discussed in the accepted answer.

Upvotes: 1

Views: 223

Answers (1)

Neil Lunn
Neil Lunn

Reputation: 151132

You cannot do this sort of projection using .find() as the general projections allowed are basic field selection. What you are talking about is document re-shaping and for that you can use the $project operator with the .aggregate() method.

So by your initial example:

db.test.aggregate([
    { "$project": {
        "coords": "$systems.1.coords",
        "systems": 1
    }}
])

That will give you output like this:

{
    "_id" : ObjectId("537fe2127cb762d14e2a1007"),
    "systems" : {
            "1" : {
                    "name" : "milky_way",
                    "coords" : {
                            "x" : 1,
                            "y" : 1
                    }
            }
    },
    "coords" : {
            "x" : 1,
            "y" : 1
    }
}

Note the different field naming there as well, as if for no other reason, the version coming from .find() would result in overlapping paths ("systems" is the same) for the levels of fields you were trying to select and therefore cannot be projected as two fields the way you can do right here.

In much the same way, consider a statement like the following:

db.test.aggregate([
    { "$project": {
        "systems": {
            "1": {
                "coords": "$systems.1.coords"
            }
        },
        "systems": 1
    }}
])

So that is not telling you it's invalid, it's just that one of the results in the projection is overwriting the other as essentially at a top level they are both callled "systems".

This is basically what you end up with when trying to do something like this with the projection method available to .find(). So the essential part is you need a different field name, and this is what the aggregation framework ( though not aggregating here ) allows you to do.

Upvotes: 1

Related Questions