self referencing update using MongoDB - mongodb

I wonder if there is a way to make a self referencing update in MongoDB, so you can use object's params on a $set query. Here is an example:
> db.labels.save({"name":"label1", "test":"hello"})
> db.labels.save({"name":"label2", "test":"hello"})
> db.labels.save({"name":"label3", "test":"hello"})
> db.labels.find()
{ "_id" : ObjectId("4f1200e2f8509434f1d28496"), "name" : "label1", "test" : "hello" }
{ "_id" : ObjectId("4f1200e6f8509434f1d28497"), "name" : "label2", "test" : "hello" }
{ "_id" : ObjectId("4f1200eaf8509434f1d28498"), "name" : "label3", "test" : "hello" }
I saw that you can use this syntax on $where queries: http://www.mongodb.org/display/DOCS/Advanced+Queries#AdvancedQueries-JavascriptExpressionsand%7B%7B%24where%7D%7D
> db.myCollection.find( { a : { $gt: 3 } } );
> db.myCollection.find( { $where: "this.a > 3" } );
> db.myCollection.find("this.a > 3");
> f = function() { return this.a > 3; } db.myCollection.find(f);
So, I tried with:
db.labels.update({"test":"hola"}, {$set : {"test": this.name})
but it didn't work.
The expected result is:
{ "_id" : ObjectId("4f1200e2f8509434f1d28496"), "name" : "label1", "test" : "label1" }
{ "_id" : ObjectId("4f1200e6f8509434f1d28497"), "name" : "label2", "test" : "label2" }
{ "_id" : ObjectId("4f1200eaf8509434f1d28498"), "name" : "label3", "test" : "label3" }
Any thoughts? Thanks in advance

Update:
It can be done by now using
db.labels.updateMany(
{"test":"hola"},
[{ $set: { test: "$name" }}],
)
Old Answer
At present there is no straight way to do that. But you can workaround this by
db.labels.find({"test":"hola"}).forEach(function (doc) {
doc.test = doc.name;
db.labels.save(doc);
})

new in MongoDB 4.2
[FYI] Below approach avoids row-by-row operations (which can cause performance issues), and shifts the processing load onto the DB itself.
"Starting in MongoDB 4.2, the db.collection.update() method can
accept an aggregation pipeline that specifies the modifications to
perform." docs
The pipeline has access to each documents' fields, thus allowing self-referencial updates.
Please see the documentation on this, which includes an example of this sort of update.
Following the example from the OP's question the update would be:
db.labels.update(
{"test":"hello"},
[{ $set: { test: "$name" }}],
{ multi: true }
);
Please note that the $set used in the pipeline refers to the aggregation stage $set, and not the update operator $set.
For those familiar with the aggregate pipeline in earlier MongoDB versions: the $set stage is an alias for $addFields.

Related

MongoDB - Updating the field type of all objects within an array from String to Int32/64

I'm fairly new to mongodb so please bear with me.
As per title, what I want to achieve is to convert a specific field in all documents within an array of a document from String to Int how do i do that?
Sample Doc :
{
reviews:[
{
snid:"1242"
},
{
snid:"8392"
}
]
}
And my objective is to convert all of the snid's from String to Int32
so far i understand that we can use something like db.collection.update() but this will update a specific field, not an array.
Another attempt is
db.collection.find({},{reviews:1,_id:0},(err,doc)=>{
//How do i push it back to the document
})
But as you can tell, I'm not entirely sure on how we should push the updated document back into the same array of sorts.
Any insights will be greatly appreciated!
1) If you're using MongoDB version >= 4.2, try below query :
db.collection.update({'reviews.snid' : {$exists : true}}, [
{
$set: {
reviews: {
$map: { input: "$reviews", in: { 'snid': { $toInt: "$$this.snid" } } }
}
}
}
],{multi :true})
Above query uses Aggregation-pipeline in .update() which was introduced in version 4.2, You can also use .updateMany() instead of .update().
It works on documents of below type :
/* 1 */
{
"_id" : ObjectId("5e810f5ec16b5679b43a2f0e"),
"reviews" : [
{
"snid" : '1242'
},
{
"snid" : '8392'
}
]
}
/* 2 */
{
"_id" : ObjectId("5e810f6ac16b5679b43a310c"),
"reviews" : [
{
"snid" : '1242232'
},
{
"snid" : '8391232'
}
]
}
/* 3 */
{
"_id" : ObjectId("5e8110b1c16b5679b43a5148"),
"abc" : 1
}
/* 4 */
{
"_id" : ObjectId("5e8110c3c16b5679b43a52f9"),
"reviews" : []
}
/* 5 */
{
"_id" : ObjectId("5e811359c16b5679b43a9229"),
"reviews" : [
{
"abc" : "1"
}
]
}
But above update query will partially work if you've a doc like below :
{
"_id" : ObjectId("5e811359c16b5679b43a9230"),
"reviews" : [
{
"abc" : "1"
},
{
"snid" : "123"
}
]
}
In that case you need to use $cond to do a conditional check in $map to see if current object has key snid then convert value or else pass on the same object as is to 'reviews' array.
2) Just in Case if your MongoDB version is < 4.2/4.0 & > 3.2 - You can use .bulkWrite() :
Cause you can not use Aggregation Pipeline in update & also $toInt. So you need to do .find() to get entire docs & write code to convert these from strings to integers & use .bulkWrite() to update docs in one update DB call (You can take _id as key for each document).
3) You can also write an aggregation query on existing collection & use $out to update entire collection or write aggregation result to new collection by running just one query. I would prefer to temporarily write it to new collection to check data is correct & rename new collection to what ever is existing by naming existing with something ends with _backup used as backup.

Mongodb regex in aggregation using reference to field value

note: I'm using Mongodb 4 and I must use aggregation, because this is a step of a bigger aggregation
Problem
How to find in a collection documents that contains fields that starts with value from another field in same document ?
Let's start with this collection:
db.regextest.insert([
{"first":"Pizza", "second" : "Pizza"},
{"first":"Pizza", "second" : "not pizza"},
{"first":"Pizza", "second" : "not pizza"}
])
and an example query for exact match:
db.regextest.aggregate([
{
$match : { $expr: { $eq: [ "$first" ,"$second" ] } } }
])
I will get a single document
{
"_id" : ObjectId("5c49d44329ea754dc48b5ace"),
"first" : "Pizza", "second" : "Pizza"
}
And this is good.
But how to do the same, but with startsWith ? My plan was to use regex but seems that is not supported in aggregation so far.
With a find and a custom javascript function works fine:
db.regextest.find().forEach(
function(obj){
if (obj.first.startsWith(obj.second)){
print(obj);
}
}
)
And returns correctly:
{
"_id" : ObjectId("5c49d44329ea754dc48b5ace"),
"first" : "Pizza",
"second" : "Pizza"
}
How it's possible to get same result with aggregation framework ?
One idea is to use existing aggregation framework pipeline, out to a temp colletion and then run the find above, to get match I'm looking for. This seems to be a workaround, I hope someone have a better idea.
Edit: here the solution
db.regextest.aggregate([{
$project : {
"first" : 1,
"second" : 1,
fieldExists : {
$indexOfBytes : ['$first', '$second' , 0]
}
}
}, {
$match : {
fieldExists : {
$gt : -1
}
}
}
]);
The simplest way is to use $expr, first available in 3.6 like this:
{
$match: {
$expr: {
$eq: [
'$second',
{
$substr: ['$first', 0, { $strLenCP: '$second' }]
}
]
}
}
}
This compares the string in field second with the first N characters of first where N is the length of second string. If they are equal, then first starts with second.
4.2 adds support for $regex in aggregation expressions, but starts with is much simpler and doesn't need regular expressions.

How to search document with condition of not having exact object in array of objects?

I have a collection of persons whose schema looks like the collection of following documents.
Document: {
name:
age:
educations:[{
title:xyz,
passed_year:2005,
univercity:abc},
{
title:asd
passed_year:2007,
univercity:mno
}],
current_city:ghi
}
Now I wanna show all the persons who has not done xyz education from abc university in year 2005.
I think two possible queries for this need but not sure which one to use as both of them are giving me the output
Query 1:
db.persons.find({"education":{$ne:{$elemMatch:{"title":"xyz","passed_year":2005,"univercity":"abc"}}}})
Query 2:
db.persons.find({"education":{$not:{$elemMatch:{"title":"xyz","passed_year":2005,"univercity":"abc"}}}})
I'm quite confused about operator $ne and $not, which one should I use with $elemMatch as both of them are giving me the output.
Given this $elemMatch: {"title":"xyz","passed_year":2005,"univercity":"abc"} I think you want to exclude any documents which contain an sub document in the educations array which contains all of these pairs:
"title" : "xyz"
"passed_year" : 2005
"univercity" : "abc"
This query will achieve that:
db.persons.find({
"educations": {
$not: {
$elemMatch:{"title": "xyz", "passed_year": 2005, "univercity": "abc"}
}
}
})
In your question you wrote:
both of them are giving me the output
I suspect this is because your query is specifying education whereas the correct attribute name is educations. By specifying education you are adding a predicate which cannot be evaluated since it references a non existent document attribute so regardless of whether that predicate uses $ne or $not it will simply not be applied.
In answer to the question of which operator to use: $not or $ne: if you run the above query with .explain(true) you'll notice that the parsed query produced by Mongo is very different for each of these operators.
Using $ne
"parsedQuery" : {
"$not" : {
"educations" : {
"$eq" : {
"$elemMatch" : {
"title" : "xyz",
"passed_year" : 2005,
"univercity" : "abc"
}
}
}
}
}
Using $not:
"parsedQuery" : {
"$not" : {
"educations" : {
"$elemMatch" : {
"$and" : [
{
"passed_year" : {
"$eq" : 2005
}
},
{
"title" : {
"$eq" : "xyz"
}
},
{
"univercity" : {
"$eq" : "abc"
}
}
]
}
}
}
}
So, it looks like use of $ne causes Mongo to do something like this psuedo code ...
not educations equalTo "$elemMatch" : {"title" : "xyz", "passed_year" : 2005, "univercity" : "abc"}
... i.e. it treats the elemMatch clause as if it is the RHS of an equality operation whereas use of $not causes Mongo to actually evaluate the elemMatch clause.

MongoDB filter array element in a nested object

I have a document as follows:
{
"_id" : ObjectId("56423b2558cb340599108b35"),
"test" : {
"source" : [
{
"member" : "abc"
},
{
"member" : "xyz"
}
]
}
}
I want to filter on the array element xyz, and I am trying the following query:
db.coll.find({ "test.source.member" : "xyz" }, { "test.source.$.member" : true }).pretty()
Apparently it used to work on 2.4, on 2.6 it does not work,
On 2.4 it returned the "xyz", whereas on 2.6 it returns "abc" i.e. the first element. Is there a way to filter "abc" because eventually i want to update. BTW, i also tried with $elemMatch and it seems to give the same output "abc".
Thanks.
According to the Docs, if you are running 2.6, this should give you the correct output:
db.coll.find({ "test.source.member" : "xyz"}, { "test.source.$" : 1}).pretty()
You could extract the member by doing this:
var member = db.coll.find(
{ "test.source.member" : "xyz"},
{ "test.source.$" : 1}
).test.source[0].member;
The value of member would be:
xyz

Mongo: Import results of an aggregate query?

Sorry for the basic question, I'm new to mongo and learning my way around.
I have run an aggregate query in Mongo:
> var result = db.urls.aggregate({$group : {_id : "$pagePath"} });
> result
{ "_id" : "/section1/page1" }
{ "_id" : "/section1/page2" }
...
Type "it" for more
I would now like to save the results of this aggregate query into a new collection. This is what I've tried:
> db.agg1.insert(result);
WriteResult({ "nInserted" : 1 })
But this seems to have inserted all the rows as just one row:
> db.agg1.count()
1
> db.agg1.findOne();
{ "_id" : "/section1/page1" }
{ "_id" : "/section1/page2" }
...
Type "it" for more
How can I insert these as separate rows?
I've tried inserting the _id directly, without success:
> db.agg1.insert(result._id);
2014-12-17T15:23:26.679+0000 no object passed to insert! at src/mongo/shell/collection.js:196
Use the $out pipeline operator for that:
db.urls.aggregate([
{$group : {_id : "$pagePath"} },
{$out: "agg1"}
]);
Note that $out was added in MongoDB 2.6.