How do I interpolate a field when updating documents in MongoDB? - mongodb

I have a collection that looks like this:
{
"_id" : ObjectId("5e5777b74db43342073051e0"),
"a" : "b"
}
{
"a" : "c",
"_id" : ObjectId("5e5777f24db43342073051e4")
}
And I want to copy the value of each document’s a into a new array field so I end up with this:
{
"_id" : ObjectId("5e5777b74db43342073051e0"),
"a" : "b",
"d": ["b"]
}
{
"_id" : ObjectId("5e5777f24db43342073051e4"),
"a" : "c",
"d": ["c"]
}
I tried this command at the shell:
db.getCollection("C").updateMany({}, {
$set: {
"d.0": "$a"
}
});
But that gave me:
{
"_id" : ObjectId("5e5777b74db43342073051e0"),
"a" : "b",
"d" : {
"0" : "$a"
}
}
{
"_id" : ObjectId("5e5777f24db43342073051e4"),
"a" : "c",
"d" : {
"0" : "$a"
}
}
$$a gave me the same result. How do I write this operation?

In Mongo 4.2 the <update> document can also be an aggregation pipeline. Try this one:
db.getCollection("C").updateMany(
{},
[{
$set: {
"d": ["$a"]
}
}]
)

Related

MongoDB aggregation query based on multiple fields with similar values

I have documents that look like this:
{
"_id" : "001",
"a" : {
"b" : {
"c" : {
"custId" : "cust1"
},
"d" : {
"custId" : "cust2"
}
}
}
}
{
"_id" : "002",
"a" : {
"b" : {
"c" : {
"custId" : "cust1"
},
"d" : {
"custId" : "cust3"
}
}
}
}
{
"_id" : "003",
"a" : {
"b" : {
"c" : {
"custId" : null
},
"d" : {
"custId" : "cust2"
}
}
}
}
{
"_id" : "004",
"a" : {
"b" : {
"c" : {
"custId" : null
},
"d" : {
"custId" : "cust1"
}
}
}
}
I would like to obtain an aggregation which shows a sorted count of customer ids, ignoring null customer ids, like this:
{
"_id" : "cust1",
"count" : 3,
"records" : [
"001", "002", "004"
]
}
{
"_id" : "cust2",
"count" : 2,
"records" : [
"001", "003"
]
}
{
"_id" : "cust3",
"count" : 1,
"records" : [
"002"
]
}
I think each document needs to be broken down into 1 or 2 customer based arrays than then unwound back into documents, but I have been unable to determine a workable solution.
make an array of custId, $map to iterate loop of b after converting from object to array using $objectToArray
$unwind deconstruct custIds array
$match to filter none null custIds documents
$group by custIds and get count of total records and make unique array of _id using $addToset
db.collection.aggregate([
{
$project: {
custIds: {
$map: {
input: { $objectToArray: "$a.b" },
in: "$$this.v.custId"
}
}
}
},
{ $unwind: "$custIds" },
{ $match: { custIds: { $ne: null } } },
{
$group: {
_id: "$custIds",
count: { $sum: 1 },
records: { $addToSet: "$_id" }
}
}
])
Playground

MongoDB: what is the difference between $elemMatch and $and to find objects inside array?

Is there any logical difference between the usage of the query operator $and
db.collection.find({$and: [{"array.field1": "someValue"}, {"array.field2": 3}]})
and the usage of the projection operator $elemMatch
db.collection.find({array: {$elemMatch: {field1: "someValue", field2: 3}}})
to find documents which contain the object fields inside an array?
I will explain this with an example. Consider the collection arrays. It has a field called arr which is an array of embedded documents (with fields a and b).
Some documents in the arrays collection:
{ "_id" : 1, "arr" : [ { "a" : "a1", "b" : "b1" }, { "a" : "a2", "b" : "b2" } ] }
{ "_id" : 2, "arr" : [ { "a" : "a1", "b" : "b11" }, { "a" : "a2", "b" : "b22" } ] }
{ "_id" : 3, "arr" : [ { "a" : "a2", "b" : "b1" }, { "a" : "a", "b" : "b1" } ] }
{ "_id" : 4, "arr" : [ { "a" : "a1", "b" : "b91" }, { "a" : "a29", "b" : "b1" } ] }
I want to find all documents with the array embedded-document fields a="a1" AND b="b1". Note this must be within the same element embedded-document of the array. I use $elemMatch for this and get the desired result.
> db.arrays.find( { arr: { $elemMatch: { a: "a1", b: "b1" } } } )
==>
{ "_id" : 1, "arr" : [ { "a" : "a1", "b" : "b1" }, { "a" : "a2", "b" : "b2" } ] }
Now, if I use the $and operator like in the following query, the results are not correct. As you can see an additional document is selected. The query worked with the array embedded-document fields a="a1" OR b="b1".
> db.arrays.find({$and: [ { "arr.a": "a1" }, { "arr.b": "b1" } ] } )
==>
{ "_id" : 1, "arr" : [ { "a" : "a1", "b" : "b1" }, { "a" : "a2", "b" : "b2" } ] }
{ "_id" : 4, "arr" : [ { "a" : "a1", "b" : "b91" }, { "a" : "a29", "b" : "b1" } ] }
So, using the $and operator is NOT intended for this purpose (i.e., querying on multiple fields of an array of sub-documents).
Also, to query on an array embedded-document field (only one field) the $elemMatch is not required, for example:
> db.arrays.find( { "arr.a": "a2" } )
==>
{ "_id" : 1, "arr" : [ { "a" : "a1", "b" : "b1" }, { "a" : "a2", "b" : "b2" } ] }
{ "_id" : 2, "arr" : [ { "a" : "a1", "b" : "b11" }, { "a" : "a2", "b" : "b22" } ] }
{ "_id" : 3, "arr" : [ { "a" : "a2", "b" : "b1" }, { "a" : "a", "b" : "b1" } ] }
Your first query will find documents, where array have at least one element with field1= somevalue and at least one element with field2=3. Both elements can be different.
The second one will retrieve documents where array have at least one element matching the two conditions simultaneously.
Here's a data sample to explain :
{
array: [
{
field1: 1,
},
{
field2: 2
},
{
field1: 1,
field2: 3
},
]
},
{
array: [
{
field1: 1,
field2: 2
},
{
field2: 3
}
]
},
{
array: [
{
field1: 1,
field2: "other"
},
{
field2: 2
}
]
}
The first query
db.collection.find({"array.field1": 1,"array.field2": 2}) (equivalent
to your $and syntax)
will returne the three documents,
db.collection.find({array: {$elemMatch: {field1: 1, field2: 2}}})
will return only the second document (the only one having an element matching both criterias)
EDIT : The logical operator of the first query is OR, for the second one it's AND, at level of array element.

Update a object field in an array

In mondodb I want to update a field of an object within an array. The example database looks like this:
{
"_id" : ObjectId("5ad237559d30d918c89c7f46"),
"myArray" : [
{
"name" : "a",
"name2" : "a",
"value" : 900000 //<--- instead of this...
},
{
"name" : "b",
"name2" : "b",
"value" : 0
}
]
},
{
"_id" : ObjectId("5ad238049d30d918c89c7f47"),
"myArray" : [
{
"name" : "b",
"name2" : "b",
"value" : 0
},
{
"name" : "c",
"name2" : "a",
"value" : 0 //... I want to update this
}
]
}
I want to update the last value field by querying name:c AND name2:a. I tried it with the following instruction, but it sets the value of the first object (name:a name2:a). Does the problem lie near the $ char?
db.test.updateOne({$and:[{"myArray.name" : "c"}, {"myArray.name2" : "a"}]},
{$set:{"myArray.$.value" : 900000}})
You need to do an $elemMatch to match the specific item in the array and then you can use the positional operator:
db.test.updateOne(
{ "myArray": { $elemMatch: { "name": "c", "name2"; "a" } } },
{ $set: { "myArray.$.value": 900000 } }
);
You can use arrayFilters.
db.test.updateOne({}, {$set:{"myArray.$[element].value" : 900000}} {
multi: true,
arrayFilters: [ {$and:[{"element.name" : "c"}, {"element.name2" : "a"}]} ]
},
)
Sorry, I have no mongodb right there to test it, the query will probably need to be tuned a little

MongoDB adding new field to the sub document

I have mongodb document like below.
{
"_id" : ObjectId("57616e718ed5a017089143f2"),
"subitems" : {
"1" : "a",
"2" : "b"
}
}
I was trying to add new fields to "subitems" field.
db.items.update({ "_id" : ObjectId("57616e718ed5a017089143f2") }, { $set: { subitems: { 3: "c" } } })
Instead of updating the field, its overwriting it like
{
"_id" : ObjectId("57616e718ed5a017089143f2"),
"subitems" : {
"3" : "c"
}
}
How do i achieve result
{
"_id" : ObjectId("57616e718ed5a017089143f2"),
"subitems" : {
"1" : "a",
"2" : "b",
"3" : "c"
}
}
Use the dot notation to add the field to an embedded document:
db.items.update(
{ "_id" : ObjectId("57616e718ed5a017089143f2") },
{ "$set": { "subitems.3": "c" } }
)
More from the documentation.

Group Mongo documents by id and get the latest document by timestamp

Imagine we have the following set of documents stored in mongodb:
{ "fooId" : "1", "status" : "A", "timestamp" : ISODate("2016-01-01T00:00:00.000Z") "otherInfo" : "BAR", ... }
{ "fooId" : "1", "status" : "B", "timestamp" : ISODate("2016-01-02T00:00:00.000Z") "otherInfo" : "BAR", ... }
{ "fooId" : "1", "status" : "C", "timestamp" : ISODate("2016-01-03T00:00:00.000Z") "otherInfo" : "BAR", ... }
{ "fooId" : "2", "status" : "A", "timestamp" : ISODate("2016-01-01T00:00:00.000Z") "otherInfo" : "BAR", ... }
{ "fooId" : "2", "status" : "B", "timestamp" : ISODate("2016-01-02T00:00:00.000Z") "otherInfo" : "BAR", ... }
{ "fooId" : "3", "status" : "A", "timestamp" : ISODate("2016-01-01T00:00:00.000Z") "otherInfo" : "BAR", ... }
{ "fooId" : "3", "status" : "B", "timestamp" : ISODate("2016-01-02T00:00:00.000Z") "otherInfo" : "BAR", ... }
{ "fooId" : "3", "status" : "C", "timestamp" : ISODate("2016-01-03T00:00:00.000Z") "otherInfo" : "BAR", ... }
{ "fooId" : "3", "status" : "D", "timestamp" : ISODate("2016-01-04T00:00:00.000Z") "otherInfo" : "BAR", ... }
I'd like to get the latest status for each fooId based on timestamp. Therefore, my return would look like:
{ "fooId" : "1", "status" : "C", "timestamp" : ISODate("2016-01-03T00:00:00.000Z") "otherInfo" : "BAR", ... }
{ "fooId" : "2", "status" : "B", "timestamp" : ISODate("2016-01-02T00:00:00.000Z") "otherInfo" : "BAR", ... }
{ "fooId" : "3", "status" : "D", "timestamp" : ISODate("2016-01-04T00:00:00.000Z") "otherInfo" : "BAR", ... }
I've been trying to go about this by using aggregation using the group operator, but the part I'm wondering is there an easy way to get the whole document back from an aggregation so it looks the same as if I had used a find query? It seems you have to specify all the fields when you group, and that doesn't seem extensible if documents can have optional fields on them that may be unknown to me. The current query I have looks like this:
db.collectionName.aggregate(
[
{ $sort: { timestamp: 1 } },
{
$group:
{
_id: "$fooId",
timestamp: { $last: "$timestamp" },
status: { "$last": "$status" },
otherInfo: { "$last": "$otherInfo" },
}
}
]
)
If you are doing and aggregation, you need to do similar to SQL , which mean specify the aggregation operation per column, the only option you have is use the $$ROOT operator
db.test.aggregate(
[
{ $sort: { timestamp: 1 } },
{
$group:
{
_id: "$fooId",
timestamp: { $last: "$$ROOT" }
}
}
]
);
But that will change the output a little bit
{ "_id" : "1", "timestamp" : { "_id" : ObjectId("570e6be3e81c8b195818e7fa"),
"fooId" : "1", "status" : "A", "timestamp" :ISODate("2016-01-01T00:00:00Z"),
"otherInfo" : "BAR" } }
If you want to return the original document format, you probably need a $project stage after that
You can use the $$ROOT system variable with the $last operator to return the last document.
db.collectionName.aggregate([
{ "$sort": { "timestamp": 1 } },
{ "$group": {
"_id": "$fooId",
"last_doc": { "$last": "$$ROOT" }
}}
])
Of course this will the last document for each group as a value of a field.
{
"_id" : "2",
"doc" : {
"_id" : ObjectId("570e6df92f5bb4fcc8bb177e"),
"fooId" : "2",
"status" : "B",
"timestamp" : ISODate("2016-01-02T00:00:00Z")
}
}
If you are not happy with that output then your best bet will be to add another $group stage to the pipeline when you simply return an array of those documents using the $push accumulator operator.
db.collectionName.aggregate([
{ "$sort": { "timestamp": 1 } },
{ "$group": {
"_id": "$fooId",
"last_doc": { "$last": "$$ROOT" }
}},
{ "$group": {
"_id": null,
"result": { "$push": "$last_doc" }
}}
])
Though there is no direct way to bring back original documents and I don't see any value, but try following aggregation query:
db.collection.aggregate([
{$sort: {fooId:1, timestamp: -1}},
{$group:{_id:"$fooId", doc:{$first:"$$ROOT"}}},
{$project:{_id:0, doc:["$doc"]}}
]).forEach(function(item){
printjson(item.doc[0]);
});
This query will emit:
{
"_id" : ObjectId("570e76d5e94e6584078f02c4"),
"fooId" : "2",
"status" : "B",
"timestamp" : ISODate("2016-01-02T00:00:00.000+0000"),
"otherInfo" : "BAR"
}
{
"_id" : ObjectId("570e76d5e94e6584078f02c8"),
"fooId" : "3",
"status" : "D",
"timestamp" : ISODate("2016-01-04T00:00:00.000+0000"),
"otherInfo" : "BAR"
}
{
"_id" : ObjectId("570e76d5e94e6584078f02c2"),
"fooId" : "1",
"status" : "C",
"timestamp" : ISODate("2016-01-03T00:00:00.000+0000"),
"otherInfo" : "BAR"
}