I am trying to update all documents in a collection with random numbers.
Each document should have a different number.
My current code
db.myDoc.update(
{ rand : { $exists : false } },
{ $set : { rand: Math.random() } },
{ multi : true }
)
populates ALL documents with the SAME random value.
How to fix?
You can make use of the cursor.forEach() cursor method in the mongo shell to achieve this:
db.myDoc.find({rand: {$exists : false }}).forEach(function(mydoc) {
db.myDoc.update({_id: mydoc._id}, {$set: {rand: Math.random()}})
})
Starting in Mongo 4.4, the $function aggregation operator allows applying a custom javascript function to implement behaviour not supported by the MongoDB Query Language.
For instance, in order to update documents with a random value:
// { "x" : 1 }
// { "x" : 2 }
db.collection.updateMany(
{ rand: { $exists: false } },
[{ $set:
{ rand:
{ $function: {
body: function() { return Math.random(); },
args: [],
lang: "js"
}}
}
}]
)
// { "x" : 1, "rand" : 0.7012578283384967 }
// { "x" : 2, "rand" : 0.21041874709692365 }
$function takes 3 parameters:
body, which is the function to apply.
args, which contains the fields from the record that the function can take as parameter. In our case we don't need any reference to the document itself in order to compute a random value, thus the empty array.
lang, which is the language in which the body function is written. Only js is currently available.
Note that this is now way more efficient than a find/foreach option since everything is done server side in one pass.
Starting in Mongo 4.4.2, the $rand aggregation operator provides a random float between 0 and 1 each time it is called:
// { "x" : 1 }
// { "x" : 2 }
db.collection.updateMany(
{ rand: { $exists: false } },
[{ $set: { rand: { $rand: {} } } }]
)
// { "x" : 1, "rand" : 0.22252333583221115 }
// { "x" : 2, "rand" : 0.9811303782541574 }
Related
I have a mongodb collection with a number of objects like this:
{
"_id" : "1234",
"type" : "automatic",
"subtypes" : [
{
"_id" : "dfgd",
"name" : "test subtype",
"subjetRequired" : true,
},
{
"_id" : "dfgd",
"name" : "test subtype2",
"subjetRequired" : false,
}
],
"anotherField" : "some value"
}
As you can see, one of the keys in the subtypes array is incorrectly spelled - "subjetRequired" instead of "subjectRequired".
I want to correct that key name. How can I do that.
I'll preface this by saying I've not worked with mongodb very much in the past.
After a lot of researching, the best I could come up with is the following (which doesn't work):
function remap(doc) {
subtypes = doc.subtypes;
var count = 0;
subtypes.forEach(function(subtype){
db.taskType.update({"_id": subtype._id}, {
$set: {"subtypes.subjectRequired" : subtype.subjetRequired},
$unset: {"subtypes.subjetRequired": 1}
});
}
)
}
db.taskType.find({"subtypes.subjetRequired":{$ne:null}}).forEach(remap);
This doesn't work.
I know the loop is correct, as if I replace the other logic with print statements I can access and print the fields who's names I want to modify.
What am I doing wrong here?
You can use this update and avoid using any code, it's also stable so you can execute it multiple times with no fear.
db.collection.updateMany({
"subtypes.subjetRequired": {
$exists: true
}
},
[
{
$set: {
subtypes: {
$map: {
input: "$subtypes",
in: {
$mergeObjects: [
"$$this",
{
subjectRequired: "$$this.subjetRequired",
}
]
}
}
}
}
},
{
$unset: "subtypes.subjetRequired"
}
])
Mongo Playground
I could modify your loop to override the whole array of subtypes:
function remap(doc) {
correctSubtypes = doc.subtypes.map(({ subjetRequired, ...rest }) => ({
...rest,
subjectRequired: subjetRequired,
}));
var count = 0;
db.taskType.findByIdAndUpdate(doc._id, {
$set: {
subtypes: correctSubtypes,
},
});
}
Schema
{
useLimit: Number,
uses: [{...}]
}
I'd like to query this collection matching only those documents where the length of uses is less than useLimit or useLimit is -1 (representing infinite)
One way of doing it, is to make use of the $where operator in the find() query:
// pass a function to the `$where` operator, to pick records
// if they meet the desired condition.
db.collection.find( {
$where : function() {
return (this.uses.length < this.useLimit || this.useLimit == -1);
}
})
or using the aggregation pipeline,
Project a field isValid for each document, with its value resolving
to true or false. true, if the condition is met, else false.
The next step is to match all the documents with the isValid
attribute with value true.
Finally the $project operator projects the fields other than
isValid.
as below,
db.collection.aggregate( [ {
$project : {
"uses" : 1,
"useLimit" : 1,
"isValid" : {
$cond : [ {
$or : [ {
$gte : [ "$useLimit", {
$size : "$uses"
} ]
}, {
$eq : [ "$useLimit", -1 ]
} ]
}, true, false ]
}
}
}, {
$match : {
"isValid" : true
}
}, {
$project : {
"uses" : 1,
"useLimit" : 1
}
} ])
I have the following documents:
[{
"_id":1,
"name":"john",
"position":1
},
{"_id":2,
"name":"bob",
"position":2
},
{"_id":3,
"name":"tom",
"position":3
}]
In the UI a user can change position of items(eg moving Bob to first position, john gets position 2, tom - position 3).
Is there any way to update all positions in all documents at once?
You can not update two documents at once with a MongoDB query. You will always have to do that in two queries. You can of course set a value of a field to the same value, or increment with the same number, but you can not do two distinct updates in MongoDB with the same query.
You can use db.collection.bulkWrite() to perform multiple operations in bulk. It has been available since 3.2.
It is possible to perform operations out of order to increase performance.
From mongodb 4.2 you can do using pipeline in update using $set operator
there are many ways possible now due to many operators in aggregation pipeline though I am providing one of them
exports.updateDisplayOrder = async keyValPairArr => {
try {
let data = await ContestModel.collection.update(
{ _id: { $in: keyValPairArr.map(o => o.id) } },
[{
$set: {
displayOrder: {
$let: {
vars: { obj: { $arrayElemAt: [{ $filter: { input: keyValPairArr, as: "kvpa", cond: { $eq: ["$$kvpa.id", "$_id"] } } }, 0] } },
in:"$$obj.displayOrder"
}
}
}
}],
{ runValidators: true, multi: true }
)
return data;
} catch (error) {
throw error;
}
}
example key val pair is: [{"id":"5e7643d436963c21f14582ee","displayOrder":9}, {"id":"5e7643e736963c21f14582ef","displayOrder":4}]
Since MongoDB 4.2 update can accept aggregation pipeline as second argument, allowing modification of multiple documents based on their data.
See https://docs.mongodb.com/manual/reference/method/db.collection.update/#modify-a-field-using-the-values-of-the-other-fields-in-the-document
Excerpt from documentation:
Modify a Field Using the Values of the Other Fields in the Document
Create a members collection with the following documents:
db.members.insertMany([
{ "_id" : 1, "member" : "abc123", "status" : "A", "points" : 2, "misc1" : "note to self: confirm status", "misc2" : "Need to activate", "lastUpdate" : ISODate("2019-01-01T00:00:00Z") },
{ "_id" : 2, "member" : "xyz123", "status" : "A", "points" : 60, "misc1" : "reminder: ping me at 100pts", "misc2" : "Some random comment", "lastUpdate" : ISODate("2019-01-01T00:00:00Z") }
])
Assume that instead of separate misc1 and misc2 fields, you want to gather these into a new comments field. The following update operation uses an aggregation pipeline to:
add the new comments field and set the lastUpdate field.
remove the misc1 and misc2 fields for all documents in the collection.
db.members.update(
{ },
[
{ $set: { status: "Modified", comments: [ "$misc1", "$misc2" ], lastUpdate: "$$NOW" } },
{ $unset: [ "misc1", "misc2" ] }
],
{ multi: true }
)
Suppose after updating your position your array will looks like
const objectToUpdate = [{
"_id":1,
"name":"john",
"position":2
},
{
"_id":2,
"name":"bob",
"position":1
},
{
"_id":3,
"name":"tom",
"position":3
}].map( eachObj => {
return {
updateOne: {
filter: { _id: eachObj._id },
update: { name: eachObj.name, position: eachObj.position }
}
}
})
YourModelName.bulkWrite(objectToUpdate,
{ ordered: false }
).then((result) => {
console.log(result);
}).catch(err=>{
console.log(err.result.result.writeErrors[0].err.op.q);
})
It will update all position with different value.
Note : I have used here ordered : false for better performance.
I'm trying to do a mongo query where I get the length of an array in each document, without retrieving the full contents of the list. Ideally, this would be a projection option along these lines:
db.log.find({},{entries:{$length: 1}})
but this isn't supported. Maybe this is possible in an elegant way with the new aggregation framework? What I've come up with is this:
db.log.find({},{"entries.length": 1})
Which returns results like this:
{ "_id" : ObjectId("50d2fb07e64cfa55431de693"), "entries" : [ { }, { }, { }, { }, { }, { }, { }, { }, { }, { }, { }, { }, { }, { }, { }, { }, { }, { } ] }
This is ugly but basically serves my needs since I can count the length of this list without the network weight of getting the full contents. But I have no idea why this works. What is this query actually doing?
Now, I could think in two approachs:
1) Using aggregation framework:
db.log.aggregate([ { $unwind : "$entries" }, { $group : { _id : "$_id", entries : {$sum:1} } } ]);
2) Or you can add a field to the document that holds the entries count. So, each time that you push a new value to entries array, you must increment the counter. The update will be like this:
db.log.update({ _id : 123 }, { $push : { entries : 'value' }, $inc : { entriesCount : 1 } })
Clearly, you have a trade-off here: the aggregation framework is too expensive for this simple operation. But adding a field to document, every update should increment the counter.
IMHO, the counter looks more reasonable, though it looks a workaround.
According to the mongodb documentation:
You can use as well $size:
db.log.aggregate([{$project:{'_id':1, 'count':{$size: "$entriesCount"}}}]);
Lets say I have stream data from the Twitter API, and I have the data stored as documents in the MongoDB. What I'm trying to find is the count of screen_name under entities.user_mentions.
{
"_id" : ObjectId("50657d5844956d06fb5b36c7"),
"contributors" : null,
"text" : "",
"entities" : {
"urls" : [ ],
"hashtags" : [
{
"text" : "",
"indices" : [
26,
30
]
},
{
"text" : "",
"indices" : []
}
],
"user_mentions" : [
{
"name":"Twitter API",
"indices":[4,15],
"screen_name":"twitterapi",
"id":6253282, "id_str":"6253282"
}]
},
...
I have attempted to use map reduce:
map = function() {
if (!this.entities.user_mentions.screen_name) {
return;
}
for (index in this.entities.user_mentions.screen_name) {
emit(this.entities.user_mentions.screen_name[index], 1);
}
}
reduce = function(previous, current) {
var count = 0;
for (index in current) {
count += current[index];
}
return count;
}
result = db.runCommand({
"mapreduce" : "twitter_sample",
"map" : map,
"reduce" : reduce,
"out" : "user_mentions"
});
But its not quite working...
Since entities.user_mentions is an array, you want to emit a value for each screen_name in the map():
var map = function() {
this.entities.user_mentions.forEach(function(mention) {
emit(mention.screen_name, { count: 1 });
})
};
Then count the values by unique screen_name in the reduce():
var reduce = function(key, values) {
// NB: reduce() uses same format as results emitted by map()
var result = { count: 0 };
values.forEach(function(value) {
result.count += value.count;
});
return result;
};
Note: to debug your map/reduce JavaScript functions, you can use print() and printjson() commands. The output will appear in your mongod log.
EDIT: For comparison, here is an example using the new Aggregation Framework in MongoDB 2.2:
db.twitter_sample.aggregate(
// Project to limit the document fields included
{ $project: {
_id: 0,
"entities.user_mentions" : 1
}},
// Split user_mentions array into a stream of documents
{ $unwind: "$entities.user_mentions" },
// Group and count the unique mentions by screen_name
{ $group : {
_id: "$entities.user_mentions.screen_name",
count: { $sum : 1 }
}},
// Optional: sort by count, descending
{ $sort : {
"count" : -1
}}
)
The original Map/Reduce approach is best suited for a large data set, as is implied with Twitter data. For a comparison of Map/Reduce vs Aggregation Framework limitations see the related discussion on the StackOverflow question MongoDB group(), $group and MapReduce.