Given a collection of Users:
db.users.insertMany(
[
{
_id: 1,
name: "sue",
points: [
{ points: 85, bonus: 20 },
{ points: 85, bonus: 10 }
]
},
{
_id: 2,
name: "bob",
points: [
{ points: 85, bonus: 20 },
{ points: 64, bonus: 12 }
]
}]);
How do I add an attribute bonus_raw in every points, with a copy of the value of bonus value? I tried:
db.getCollection('users').update({ },
{$set:{ 'points.$.bonus_raw' : 'points.$.bonus' }}, false, true)
but I get:
The positional operator did not find the match needed from the query. Unexpanded update: points.$.bonus_raw
Updating multiple items in an array is not possible as of now in MongoDB.
To get this done, you will have to query the document, loop over all of your nested documents, and then save it back to MongoDB.
In your case, this can help:-
db.users.find({points: { $exists: true } }).forEach(function (doc){
doc.points.forEach(function (points) {
points.bonus_raw = points.bonus;
});
db.users.save(doc)
});
Also, take care of race conditions while doing an update in this way. See this
Related
It's been 2 days (or nights should I say) since I am trying to figure out following so would appreciate your help guys.
in mongodb I have number of orders (I will simplify documents for the case).
I want to group all documents by $campRoundId and where there are installments, push all object to installments variable.
The problem I am facing is when document.installments array is empty it pushes it to the array too.
Initial documents (same campRoundId to group by - first with no instalmments, second with 2)
[
{
_id: ObjectId("62792d8a519af6ae8cdff779"),
campRoundId: ObjectId("620a790b2cbc52006c83115a"),
installments: [],
},
{
_id: ObjectId("62792d8a519af6ae8cdff77a"),
campRoundId: ObjectId("620a790b2cbc52006c83115a"),
installments: [
{
payment: 100,
paymentStatus: false,
},
{
payment: 20,
paymentStatus: false,
},
],
},
];
my aggregation
/**
* _id: The id of the group.
* fieldN: The first field name.
*/
{
_id : "$campRoundId",
installments: {
$push: {
$cond:[
{ $gte: ["$installments.length", 1] },
"$installments",
null
]
}
}
}
I want to get rid of empty object, so if there are no installments nothing will be pushed. (dotted lines)
I have this fairly simple Chat object:
{
_id: ObjectId('a4bd8c2f5g19b0a1d1'),
chatId: 1,
userId1: 100,
userId2: 234,
messages: [
{msgId: 1, senderId: 100, msgSentOnUnix: 1652046779, msgReadOnUnix: 1652046787, content: 'hello'},
{msgId: 2, senderId: 100, msgSentOnUnix: 1652046786, msgReadOnUnix: 1652046787, content: 'world'},
{msgId: 3, senderId: 234, msgSentOnUnix: 1652046795, msgReadOnUnix: -1, content: 'right back...'},
{msgId: 4, senderId: 234, msgSentOnUnix: 1652046802, msgReadOnUnix: -1, content: 'at you'},
]
}
It's a chat between 2 people, with a chatId, both users' IDs, and all their messages.
I wish to update many sub-documents, and get the count of how many sub-documents were updated, in one single update query.
To be more specific:
I want to update user XXX's messages as "read" once user YYY enters the "chatroom", and I want to know how many "unread" messages were there. I don't want to do this with 2 queries, as I am concerned with concurrency issues...
Right now, for the update alone (without the count) I use this update command:
db.chats.updateOne(
{
userID1: 100,
userID2: 234,
},
{
$set: {
'messages.$[i].msgReadOnUnix': $$NOW
}
},
{
arrayFilters: [
{
$and: [
{ 'i.senderID': 234 }, // user 100 is reading users 234's messages
{ 'i.msgReadOnUnix': -1 }, // -1 Marks that a message is unread
]
}
]
},
)
Could a count be returned as a result here? to state how many sub-documents were modified?
So I eventually overcame the issue the way #prasad_ had mentioned,
the approach of running the test twice!
Once inside mongoDB, and once on the application level.
I.E. I used findOneAndUpdate({}) to return the document, without the { new: true } property, in order to get the document prior to the update, and then ran the same check that mongo ran:
(i am using node JS)
const { error, errorMsg, data } = await Chat.findOneAndUpdate(
{
userID1: 100,
userID2: 234,
},
{
$set: {
'messages.$[i].msgReadOnUnix': new Date().getTime(),
},
},
{
arrayFilters: [
{
$and: [{ 'i.senderID': 234}, { 'i.msgReadOnUnix': -1 }],
},
],
},
)
.lean()
.then((resultSet) => ({ error: false, data: resultSet }))
.catch((errorMsg) => ({ error: true, errorMsg }));
// running the check again here on the application level ↓
for(let i=0; i < ... ; i++){
// write check logic here and count
}
Since it's the same document coming in it solves the concurrency issue that I was so afraid of. While this works great, can anyone think of a better way? One where I don't have to run my check twice?
Feels like there should be a better way query-wise to be able to solve this...
Document structure in cities collection is like this
cities
{
_id: ObjectId("5e78ec62bb5b406776e92fac"),
city_name: "Mumbai",
...
...
subscriptions: [
{
_id: 1,
category: "Print Magazine",
subscribers: 183476
options: [
{
name: "Time",
subscribers: 56445
},
{
name: "The Gentlewoman",
subscribers: 9454
},
{
name: "Gourmand",
subscribers: 15564
}
...
...
]
},
{
_id: 2,
category: "RSS Feed",
subscribers: 2645873
options: [
{
name: "Finance",
subscribers: 168465
},
{
name: "Politics",
subscribers: 56945
},
{
name: "Entrepreneurship",
subscribers: 56945
},
...
...
]
}
]
}
Now when a user subscribes like below
{
cityId: 5e78ec62bb5b406776e92fac
selections: [
{
categoryId: 1,
options : ["Time", "Gourmand"]
},
{
categoryId: 2,
selected: ["Politics", "Entrepreneurship"]
}
]
}
I want to update the following in the cities document
Increment subscribers for "Print Magazine" by 1
Increment subscribers for "Time" by 1
Increment subscribers for "Gourmand" by 1
Increment subscribers for "RSS Feed" by 1
Increment subscribers for "Politics" by 1
Increment subscribers for "Entrepreneurship" by 1
So when an item is subscribed, its subscribers count is incremented by 1. And the category it falls into, its subscriber count is also incremented by 1.
I want to achieve this in a single update query. Any tips how can I do this?
Use case details
Each user's subscription details are stored in user_subscription_details collection(not listed here). subscriptions property in cities holds just the subscription summary for each city.
So I was able to it with the following query
db.cities.updateOne(
{
_id : ObjectId("5e78ec62bb5b406776e92fac")
},
{
$inc: {
"subscriptions.$[category].subscribers" : 1,
"subscriptions.$[category].options.$[option].subscribers" : 1
}
},
{ multi: true,
arrayFilters: [
{ "category._id": {$in: ["1", "2"]} },
{ "option.name": {$in: ["Time", "Gourmand", "Politics", "Entrepreneurship"]} }
]
}
)
Brief Explanation
First the document is matched with _id.
In update block we will declare the fields to be updated
"subscriptions.$[?].subscribers" : 1,
"subscriptions.$[?].options.$[?].subscribers" : 1
I have used ? here to show we don't know yet for which elements in the array we need to do these update. Which we can declare in the next block by filtering the array elements that need to be updated.
In filter block we filter array elements on some condition
{ "category._id": {$in: ["1", "2"]} }
{ "option.name": {$in: ["Time", "Gourmand", "Politics", "Entrepreneurship"]} }
First we filter the elements in the outer array by _id i.e only subscription categories whose _id is either 1 or 2.
Next, we filter the elements in the inner options array on the name field. Elements which will pass both filters will get updated.
Note: category in category._id and option in option.name can be any name. But the same name is to be used for fields path in update block.
For, Spring Boot MongoOperation translation of this query look at this answer
Is there a way to conditionally $addToSet based on a specific key field in a subdocument on an array?
Here's an example of what I mean - given the collection produced by the following sample bootstrap;
cls
db.so.remove();
db.so.insert({
"Name": "fruitBowl",
"pfms" : [
{
"n" : "apples"
}
]
});
n defines a unique document key. I only want one entry with the same n value in the array at any one time. So I want to be able to update the pfms array using n so that I end up with just this;
{
"Name": "fruitBowl",
"pfms" : [
{
"n" : "apples",
"mState": 1111234
}
]
}
Here's where I am at the moment;
db.so.update({
"Name": "fruitBowl",
},{
// not allowed to do this of course
// "$pull": {
// "pfms": { n: "apples" },
// },
"$addToSet": {
"pfms": {
"$each": [
{
"n": "apples",
"mState": 1111234
}
]
}
}
}
)
Unfortunately, this adds another array element;
db.so.find().toArray();
[
{
"Name" : "fruitBowl",
"_id" : ObjectId("53ecfef5baca2b1079b0f97c"),
"pfms" : [
{
"n" : "apples"
},
{
"n" : "apples",
"mState" : 1111234
}
]
}
]
I need to effectively upsert the apples document matching on n as the unique identifier and just set mState whether or not an entry already exists. It's a shame I can't do a $pull and $addToSet in the same document (I tried).
What I really need here is dictionary semantics, but that's not an option right now, nor is breaking out the document - can anyone come up with another way?
FWIW - the existing format is a result of language/driver serialization, I didn't choose it exactly.
further
I've gotten a little further in the case where I know the array element already exists I can do this;
db.so.update({
"Name": "fruitBowl",
"pfms.n": "apples",
},{
$set: {
"pfms.$.mState": 1111234,
},
}
)
But of course that only works;
for a single array element
as long as I know it exists
The first limitation isn't a disaster, but if I can't effectively upsert or combine $addToSet with the previous $set (which of course I can't) then it the only workarounds I can think of for now mean two DB round-trips.
The $addToSet operator of course requires that the "whole" document being "added to the set" is in fact unique, so you cannot change "part" of the document or otherwise consider it to be a "partial match".
You stumbled on to your best approach using $pull to remove any element with the "key" field that would result in "duplicates", but of course you cannot modify the same path in different update operators like that.
So the closest thing you will get is issuing separate operations but also doing that with the "Bulk Operations API" which is introduced with MongoDB 2.6. This allows both to be sent to the server at the same time for the closest thing to a "contiguous" operations list you will get:
var bulk = db.so.initializeOrderedBulkOp();
bulk.find({ "Name": "fruitBowl", "pfms.n": "apples": }).updateOne({
"$pull": { "pfms": { "n": "apples" } }
});
bulk.find({ "Name": "fruitBowl" }).updateOne({
"$push": { "pfms": { "n": "apples", "state": 1111234 } }
})
bulk.execute();
That pretty much is your best approach if it is not possible or practical to move the elements to another collection and rely on "upserts" and $set in order to have the same functionality but on a collection rather than array.
I have faced the exact same scenario. I was inserting and removing likes from a post.
What I did is, using mongoose findOneAndUpdate function (which is similar to update or findAndModify function in mongodb).
The key concept is
Insert when the field is not present
Delete when the field is present
The insert is
findOneAndUpdate({ _id: theId, 'likes.userId': { $ne: theUserId }},
{ $push: { likes: { userId: theUserId, createdAt: new Date() }}},
{ 'new': true }, function(err, post) { // do the needful });
The delete is
findOneAndUpdate({ _id: theId, 'likes.userId': theUserId},
{ $pull: { likes: { userId: theUserId }}},
{ 'new': true }, function(err, post) { // do the needful });
This makes the whole operation atomic and there are no duplicates with respect to the userId field.
I hope this helpes. If you have any query, feel free to ask.
As far as I know MongoDB now (from v 4.2) allows to use aggregation pipelines for updates.
More or less elegant way to make it work (according to the question) looks like the following:
db.runCommand({
update: "your-collection-name",
updates: [
{
q: {},
u: {
$set: {
"pfms.$[elem]": {
"n":"apples",
"mState": NumberInt(1111234)
}
}
},
arrayFilters: [
{
"elem.n": {
$eq: "apples"
}
}
],
multi: true
}
]
})
In my scenario, The data need to be init when not existed, and update the field If existed, and the data will not be deleted. If the datas have these states, you might want to try the following method.
// Mongoose, but mostly same as mongodb
// Update the tag to user, If there existed one.
const user = await UserModel.findOneAndUpdate(
{
user: userId,
'tags.name': tag_name,
},
{
$set: {
'tags.$.description': tag_description,
},
}
)
.lean()
.exec();
// Add a default tag to user
if (user == null) {
await UserModel.findOneAndUpdate(
{
user: userId,
},
{
$push: {
tags: new Tag({
name: tag_name,
description: tag_description,
}),
},
}
);
}
This is the most clean and fast method in the scenario.
As a business analyst , I had the same problem and hopefully I have a solution to this after hours of investigation.
// The customer document:
{
"id" : "1212",
"customerCodes" : [
{
"code" : "I"
},
{
"code" : "YK"
}
]
}
// The problem : I want to insert dateField "01.01.2016" to customer documents where customerCodes subdocument has a document with code "YK" but does not have dateField. The final document must be as follows :
{
"id" : "1212",
"customerCodes" : [
{
"code" : "I"
},
{
"code" : "YK" ,
"dateField" : "01.01.2016"
}
]
}
// The solution : the solution code is in three steps :
// PART 1 - Find the customers with customerCodes "YK" but without dateField
// PART 2 - Find the index of the subdocument with "YK" in customerCodes list.
// PART 3 - Insert the value into the document
// Here is the code
// PART 1
var myCursor = db.customers.find({ customerCodes:{$elemMatch:{code:"YK", dateField:{ $exists:false} }}});
// PART 2
myCursor.forEach(function(customer){
if(customer.customerCodes != null )
{
var size = customer.customerCodes.length;
if( size > 0 )
{
var iFoundTheIndexOfSubDocument= -1;
var index = 0;
customer.customerCodes.forEach( function(clazz)
{
if( clazz.code == "YK" && clazz.changeDate == null )
{
iFoundTheIndexOfSubDocument = index;
}
index++;
})
// PART 3
// What happens here is : If i found the indice of the
// "YK" subdocument, I create "updates" document which
// corresponds to the new data to be inserted`
//
if( iFoundTheIndexOfSubDocument != -1 )
{
var toSet = "customerCodes."+ iFoundTheIndexOfSubDocument +".dateField";
var updates = {};
updates[toSet] = "01.01.2016";
db.customers.update({ "id" : customer.id } , { $set: updates });
// This statement is actually interpreted like this :
// db.customers.update({ "id" : "1212" } ,{ $set: customerCodes.0.dateField : "01.01.2016" });
}
}
}
});
Have a nice day !
I have items like these in my collection
{
user: data,
somestuff: [somedata ...],
terminals: [ {
label: data,
details: [{more content}, {}, ...]
}]
}
I would use 'find' to extract "details" field for a specific terminal 'label'
I know that there is an easy way to get the "terminals" array by:
collection.find({_id: "..."}, {_id:0, terminals: 1})
Wich return
{ terminals: [ {
label: data,
details: [{more content}, {}, ...]
}]
}
I tried
collection.find({ "terminals.label": data }, { _id: 0, "terminals.$.details": 1 })
As edirican has suggested
And it almost work, but it return the same structure than previously except that the terminals list contain only the labeled document
The result I expect is the details list, extracted from terminals
{ details: [{more content}, {}, ...] }
Thanks for your help !
Use positional ($) projection.
http://docs.mongodb.org/manual/reference/operator/projection/positional/
collection.find({ "terminals.label": 2 }, { _id: 0, "terminals.$.details": 1 })