Okay, the title is a bit un-detailed (you could only fit so much in there) but I will explain, so imagine this structure;
{"_id": 123}
Easy right? So what I am doing is adding a field when it does not detect a duplicate, for example if id 123 got the fruit; apple with an amount counter and displaying if it is healthy, then apple will be added into the field, simple, but I'm not done, here it is so far;
{"_id": 123, "Apple": {"Healthy?": "Yes", "amount": 1}}
Now let's say id 123 decides to get him/her a pizza, then the field will once again update;
{"_id": 123, "Apple": {"Healthy?": "Yes", "amount": 1}, "Pizza": {"Healthy?": No, "amount": 1}}
So far, very straightforward, but what if id 123 gets himself another apple? Well also very simple I just used; collection.update({"_id": 123},{"$inc": {"Apple.amount": 1}}) this now turns into;
{"_id": 123, "Apple": {"Healthy?": "Yes", "amount": 2}, "Pizza": {"Healthy?": No, "amount": 1}}
Now if we try to implement both of these together; i.e; detecting if a similar field has been inserted, then update the value, or detect that no fields exist under this id, insert the new data. Right now I have;
try:
collection.update({"_id": user},{"$inc": {f"{thing}.amount": 1}})
except:
collection.update_one({"_id" : user},{"$set":{f"{thing}": idf}})
People who don't know python or doesn't understand this be like; whoa whoa whoa this was a cakewalk until this showed up. Allow me to explain, what is happening is I am getting the user id as {user}, so if user 123 were to use this, then it would be {"_id": "123"} but I have that taken care of, {thing} is the object obtained, if user 123 gets an apple then {thing} would be Apple. In the second update, there is {idf} this is basically how I get the data for each object, having a json store this and pulling the object data and inserting this. Now what this code does right now is it just inserts {"_id": 123, "Apple": {"amount": 1}} if you didn't know, using $inc will create a new field if it does not detect a current field, so it will try to update it, since it succeeds nothing else will happen. What I want this to do is to update the object amount, if it does not manage to detect a file similar to the format, then insert a fresh field with my data on the json. What I have done is try and find the object under the id and then according to that update adaptively, but I have tried so many ways to get this to work, what I thought would work the most is find = collection.find({"_id": user, f"{thing}": {}} ), but it just doesn't work, it doesn't even bother to find anything because there is an error.
Can anyone take a stab at this?
You can use update with aggregation pipeline starting from MongoDB 4.2,
Assume you have the below input data:
user = 123
thing = "Apple"
idf = {
"Healthy?": "Yes",
"amount": 1
}
The query would be,
$cond to check if thing is not missing type
it's not missing then increment amount by $add and merge default properties with an incremented amount using $mergeObjects
if it's missing then add thing new properties
collection.update(
{ '_id': user },
[{
'$set': {
f"{thing}": {
'$cond': [
{ '$ne': [{ '$type': f"${thing}" }, "missing"] },
{
'$mergeObjects': [
f"${thing}",
{ 'amount': { '$add': [f"${thing}.amount", 1] } }
]
},
idf
]
}
}
}]
)
Playground
Related
I am new to MongoDB and I use Atlas & Charts in order to query and visualize the results.
I want to create a graph that shows the max amount of money every day, and indicate the person with the max amount of money.
for example:
if my collection contains the following documents:
{"date": "15-12-2020", "name": "alice", "money": 7}
{"date": "15-12-2020", "name": "bob", "money": 9}
{"date": "16-12-2020", "name": "alice", "money": 39}
{"date": "16-12-2020", "name": "bob", "money": 25}
what should be the query I put on query box (on "Charts") in order to create a graph with the following result?
date | max_money | the_person_with_max_money
15-12-2020 9 bob
16-12-2020 39 alice
You have to use an aggregation and I think this should works.
First of all $sort values by money (I'll explain later why).
And then use $group to group values by date.
The query looks like this:
db.collection.aggregate([
{
"$sort": { "money": -1 }
},
{
"$group": {
"_id": "$date",
"max_money": { "$max": "$money" },
"the_person_with_max_money": { "$first": "$name" }
}
}
])
Example here
How this works? Well, there is a "problem" using $group, is that you can't keep values for the next stage unless you uses an accumulator, so, the best way it seems is to use $first to get the first name.
And this is why is sorted by money descendent, to get the name whose money value is the greatest at first position.
So, sorting we ensure that the first value is what you want.
And then using group to group the documents with the same date and create the fields max_money and the_person_with_max_money.
I'm pretty new to MongoDB and while preparing data to be consumed I got into Aggregation... what a powerful little thing this database has! I got really excited and started to test some things :)
I'm saving time entries for a companyId and employeeId ... that can have many entries... those are normally sorted by date, but one date can have several entries (multiple registrations in the same day)
I'm trying to come up with a good schema so I could easily get my data exactly how I need and as a newbie, I would rather ask for guidance and check if I'm in the right path
my output should be as
[{
"company": "474A5D39-C87F-440C-BE99-D441371BF88C",
"employee": "BA75621E-5D46-4487-8C9F-C0CE0B2A7DE2",
"name": "Bruno Alexandre":
"registrations": [{
"id": 1448364,
"spanned": false,
"spannedDay": 0,
"date": "2019-01-17",
"timeStart": "09:00:00",
"timeEnd": "12:00:00",
"amount": {
"days": 0.4,
"hours": 2,
"km": null,
"unit": "days and hours",
"normHours": 5
},
"dateDetails": {
"week": 3,
"weekDay": 4,
"weekDayEnglish": "Thursday",
"holiday": false
},
"jobCode": {
"id": null,
"isPayroll": true,
"isFlex": false
},
"payroll": {
"guid": null
},
"type": "Sick",
"subType": "Sick",
"status": "APP",
"reason": "IS",
"group": "LeaveAndAbsence",
"note": null,
"createdTimeStamp": "2019-01-17T15:53:55.423Z"
}, /* more date entries */ ]
}, /* other employees */ ]
what is the best way to add the data into a collection?
Is it more efficient if I create a document per company/employee and add all registration entries inside that document (it could get really big as time passes)... or is it better to have one document per company/employee/date and add all daily events in that document instead?
regarding aggregation, I'm still new to all this, but I'm imagining I could simply call
RegistrationsModel.aggregate([
{
$match: {
date: { $gte: new Date('2019-01-01'), $lte: new Date('2019-01-31') },
company: '474A5D39-C87F-440C-BE99-D441371BF88C'
}
},
{
$group: {
_id: '$employee',
name: { '$first': '$name' }
}
},
{
// ... get all registrations as an Array ...
},
{
$sort: {
'registrations.date': -1
}
}
]);
P.S. I'm taken the Aggregation course to start familiarized with all of it
Is it more efficient if I create a document per company/employee and
add all registration entries inside that document (it could get really
big as time passes)... or is it better to have one document per
company/employee/date and add all daily events in that document
instead?
From what I understand of document oriented databases, I would say the aim is to have all the data you need, in a specific context, grouped inside one document.
So what you need to do is identify what data you're going to need (getting close to the features you want to implement) and build your data structure according to that. Be sure to identify future features, cause the more you prepare your data structure to it, the less it will be tricky to scale your database to your needs.
Your aggregation query looks ok !
I'm trying to update a field in a collection with data from the same collection, but from a sub-sub field in it, and either can't get the syntax right, or I'm just doing it wrong.
I've spent quite some time now digging around here, but can't seem to get anywhere.
Here's the example structure of the users collection:
{
"_id": "12345qwerty",
"services": {
"oauth": {
"CharacterID": 12345678,
"CharacterName": "Official Username",
},
},
"name": "what I want to change",
"username": "OfficialUsername"
}
What I'm trying to do would be pretty trivial with SQL, ie: update all the display names to match a trusted source...
update users
set name = services.oauth.CharacterName;
...but I'm having trouble getting in MongoDB, and I have a feeling im doing it wrong.
Here's what i have so far, but it doesn't work as expected.
db.users.find().snapshot().forEach(
function (elem) {
db.users.update(
{ _id: elem._id },
{ $set: { name: elem.services.oauth.CharacterName } }
);
}
);
I can set the name to be anything at the base level, but can't set it to be something from the sublevel, as it doesn't recognise the sub-fields.
Any help would be greatly appreciated!
db.users.update({"services.oauth.CharacterName": {$exists: true}},{$set: {"name": "services.oauth.CharacterName"}},{multi:true})
I am setting name at the root of your document to be equal to the value in services.oauth.CharacterName in the sub sub document. multi = true will update multiple document, I am only updating documents that have the services.oauth.CharacterName value.
I am learning mongodb and wondering if can I restrict push by matching values.
For example:
field1 = {
id:123,
title:123,
likes: [{by:1,type:'like'}, {by:2, type:'like'}]
}
Can I restrict push by id in likes?
What you may have already tried was the $addToSet operator, but then found out it does not suit the case here as the combination of "id" and "type" can possibly vary. For instance what you don't want is the same "id" value with both types "like" and "dislike".
This is however a typical "voting" model, and the current structure is not the best one. A better model for this is as so, with the basic fields just for example:
{
"_id": 123,
"likeCount": 2,
"dislikeCount": 0,
"likes": [456,789]
"dislikes": []
}
Having seperate arrays is important to the atomic update process, since you cannot both $pull and $push from an array. But more than that, as it re-enforces the logic behind keeping the "count" values, as this is useful for simple queries as sorting as opposed to calculating array length.
In order to post a "like" for a user who you don't want to duplicate in the array, the $addToSet operator is still not be best one despite the values now being truly unique. You want to contrain the "count" as well, so add the conditions to the query in the update instead:
db.collection.update(
{ "_id": 123, "likes": { "$ne": 456 } },
{
"$push": { "likes": 456 },
"$inc": { "likeCount": 1 }
}
)
That way, if the user has already voted their "like" then not only is nothing added but the "count" is kept at the correct total as well. Basically the query condition on the update was not met as there already was an element in the array matching that value. So the document does not match and nothing is updated.
That is a good approach, but we can make that better still. What if the user already posted to "dislike" and now changes their mind to "like" instead? What you really need here are "two" update statements to cover the possible conditions, and this is where the Bulk Operations API comes in, to handle that logic in a single request:
var bulk = db.collection.initializeOrderedBulkOp();
// match and update where a dislike is present
bulk.find({
"_id": 123,
"likes": { "$ne": 456 },
"dislikes": 456
}).updateOne({
"$push": { "likes": 456 },
"$pull": { "dislikes": 456 }
"$inc": {
"likeCount": 1,
"dislikeCount": -1
}
});
// match and update where no dislike exists
bulk.find({
"_id": 123,
"likes": { "$ne": 456 },
"dislikes": { "$ne": 456 }
}).updateOne({
"$push": { "likes": 456 },
"$inc": { "likeCount": 1 }
});
// Send requests to server and respond
bulk.execute();
In this case if the first statement did not match because there was no dislike then nothing would be updated, but if there was a dislike then the correct adjustments would be made.
With the second request, this one would be applied if there was nothing in the dislikes array to match and there was also not a matching item in the likes array. So this would apply for a new vote and also does not conflict with the previous statement. Despite the two statements, the upadte is only ever applied once or not at all depending on the state conditions.
That is the basic pattern for handling this kind of voting properly, as you keep lists of each vote type as well as maintaining the counts for ease of access. The "dislikes" process is pretty much just the reverse of the logic for the elements you need to check for, and removing votes has similar conditions as well.
There is a way to concat more updates?
For example I would like to change more values in the same element. So having this...
{
cc: [
{ user_id: "1", hasSeen:true}
,{ user_id: "2", hasSeen:false}
,{ user_id: "3", hasSeen:false}
]
,conversation: [{
user_id: "1",
text: "message by 1, to 2and3"
}]
}
...I would like to push a new conversation object and also change all the hasSeen values.
For do the first point, no problem, I just push only a new conversation object. And it works...
...update(
{ _id : _param.conversation_id }
,{ $push:{ conversation:{user_id:"2",text:"message by 2, to 1,3"} }}
)
.exec(function(err, numAffected, rawResponse) {
});
But I would like also to change the three "hasSeen" values in the same time. is it possible?
Can I do it with one query? or i should split it in two queries?
ps: I use mongoose.
Currently the positional operator (which I think you will need here) does not work in such a manner that you can do a type of conditional update whereby you iterate through a list of $incs or $sets to change those subdocument values.
There is a JIRA for such a thing that could possibly help you: https://jira.mongodb.org/browse/SERVER-6566?page=com.atlassian.jira.plugin.system.issuetabpanels:changehistory-tabpanel but it is filed under "features we are not sure of".
Best to split this up currently.