MongoDB doesn't support matching into more than one level of an array since the positional operator only supports one level deep and only the first matching element. There is a JIRA ticket for this. For other similar issues, see Multiple use of the positional $
operator to update nested arrays.
You have a couple of options here; consider modifying your schema by flattening the structure so each document represents just one array level i.e :
A : {
B: {
C: [...]
}
}
The other option is to use a MapReduce operation that produces a collection that has documents with properties that have values of array index positions for each corresponding arrayQuery
document. The basic idea with MapReduce is that it uses JavaScript as its query language but this tends to be fairly slower than the aggregation framework and should not be used for real-time data analysis.
In your MapReduce operation, you need to define a couple of steps i.e. the mapping step (which maps each arrayQuery
B array into every document in the collection, and the operation can either do nothing or emit some object with keys and projected values) and reducing step (which takes the list of emitted values and reduces it to a single element).
For the map step, you ideally would want to get for every document in the collection, the index for each B
array field and another key that contains the $pull
keys.
Your reduce step would be a function (which does nothing) simply defined as var reduce = function() {};
The final step in your MapReduce operation will then create a separate collection named arrayUpdates
that contains the emitted operations array object along with a field with the $pull
conditions. This collection can be updated periodically when you run the MapReduce operation on the original collection.
Altogether, this MapReduce method would look like:
var map = function(){
for(var i = 0; i < this.A.B.length; i++){
emit(
{
"_id": this._id,
"index": i
},
{
"index": i,
A: {
B: this.A.B[i]
},
"update": {
"value": "A.B." + i.toString() + ".C" // this projects the $pull query with the dynamic array element positions
}
}
);
}
};
var reduce = function(){};
db.arrayQuery.mapReduce(
map,
reduce,
{
"out": {
"replace": "arrayUpdates"
}
}
);
Querying the output collection operations
from the MapReduce operation will typically give you the result:
db.arrayUpdates.findOne()
Output:
/* 1 */
{
"_id" : {
"_id" : ObjectId("5534da99180e849972938fe8"),
"index" : 0
},
"value" : {
"index" : 0,
"A" : {
"B" : {
"C" : [
1,
2,
3
]
}
},
"update" : {
"value" : "A.B.0.C"
}
}
}
You can then use the cursor from the db.arrayUpdates.find()
method to iterate over and update your collection accordingly:
var cur = db.arrayUpdates.find({"value.A.B.C": 1 });
// Iterate through results and update using the update query object set dynamically by using the array-index syntax.
while (cur.hasNext()) {
var doc = cur.next();
var update = { "$pull": {} };
// set the update query object
update["$pull"][doc.value.update.value] = 1;
db.arrayQuery.update({ "A.B.C": 1 }, update );
};
This pulls out the value 1 in the C array for each document with the query
db.arrayQuery.update({ "A.B.C": 1 }, update );
the object update
for example for the first matching document can be { $pull: {"A.B.1.C": 1} }
Hence your final result with the db.arrayQuery.find()
query after running the above will be:
/* 0 */
{
"_id" : ObjectId("5534da99180e849972938fe8"),
"A" : {
"B" : [
{
"C" : [
2,
3
]
},
{
"C" : [
2,
3
]
}
]
}
}
/* 1 */
{
"_id" : ObjectId("5534da99180e849972938fe9"),
"A" : {
"B" : [
{
"C" : [
3
]
},
{
"C" : [
3
]
}
]
}
}