I have two collections. A 'users' collection and an 'events' collection. There is a primary key on the events collection which indicates which user the event belongs to.
I would like to count how many events a user has matching a certain condition.
Currently, I am performing this like:
db.users.find({ usersMatchingACondition }).forEach(user => {
const eventCount = db.events.find({
title: 'An event title that I want to find',
userId: user._id
}).count();
print(`This user has ${eventCount} events`);
});
Ideally what I would like returned is an array or object with the UserID and how many events that user has.
With 10,000 users - this is obviously producing 10,000 queries and I think it could be made a lot more efficient!
I presume this is easy with some kind of aggregate query - but I'm not familiar with the syntax and am struggling to wrap my head around it.
Any help would be greatly appreciated!
You need $lookup to get the data from events matched by user_id. Then you can use $filter to apply your event-level condition and to get a count you can use $size operator
db.users.aggregate([
{
$match: { //users matching condition }
},
{
$lookup:
{
from: 'events',
localField: '_id', //your "primary key"
foreignField: 'user_id',
as: 'user_events'
}
},
{
$addFields: {
user_events: {
$filter: {
input: "$user_events",
cond: {
$eq: [
'$$this.title', 'An event title that I want to find'
]
}
}
}
}
},
{
$project: {
_id: 1,
// other fields you want to retrieve: 1,
totalEvents: { $size: "$user_events" }
}
}
])
There isn't much optimization that can be done without aggregate but since you specifically said that
First, instead of
const eventCount = db.events.find({
title: 'An event title that I want to find',
userId: user._id
}).count();
Do
const eventCount = db.events.count({
title: 'An event title that I want to find',
userId: user._id
});
This will greatly speed up your queries because the find query actually fetches the documents first and then does the counting.
For returning an array you can just initialize an array at the start and push {userid: id, count: eventCount} objects to it.
I have a schema which contains Friends . Friends is an array where each element is an object that contains an id, gender, and emoji.
var userSchema = new Schema({
id: {
type: String,
unique: true,
required: true
},
gender: String,
Friends: [{
id: String
gender: String
emoji: String
}]
});
In the code below, I'm accessing one document which contains a Friends array and specifying that search with distinct such it only shows the document's array of Friends. I need to access only one element of that array that contains a specified id. Instead of filtering that array after the query like what's done in the code, is there a way to just get the element purely from the query? In other words, is there an extra functionality to distinct or some kind of mongoose operator that allows that?
User.findOne().distinct('Friends', { id: req.body.myId }).then(function(myDoc) {
var friendAtt = myDoc.filter(function(obj) {
return obj.id == req.body.id
})[0]
})
Thanks to bertrand I was able to find that the answer lies in 'Projection'. In mongodb it's '$', in mongoose its select. Here is how I made it work:
User.findOne({id: req.body.myId}).select({ Friends: {$elemMatch: {id: req.body.id}}}),
It only returns the element that matched the id specified in friends.
You don't really need distinct here, you can use find() on Friends.id and filter the first subdocument that match Friends.id with the positional parameter $ :
db.user.find(
{ 'id': 'id1', 'Friends.id': 'id2'},
{ 'Friends.$': 1 }
)
In mongoose :
User.find({ 'id': req.body.myId, 'Friends.id': req.body.id }, { 'Friends.$': 1 }).then(function(myDoc) {
console.log("_id :" + myDoc[0].Friends[0].id);
console.log("gender:" + myDoc[0].Friends[0].gender);
})
I have this schema:
var eventSchema = new mongoose.Schema({
name: String,
tags: [{
tagId: mongoose.Schema.Types.ObjectId,
name: String,
description: String
}],
});
var Event = mongoose.model('Event', eventSchema);
And a array of tags ids, eg:
var arrayOfTagsIds = [23, 55, 71];
// in this example I'm not using real mongo id's, which are strings
How can I use mongoose's find to search Events that have any of these tags?
For example, this event should be in the results because it has tagId 23 and tagId 55:
{
_id: ...,
name: 'Football tournament',
tags: [{ tagId: 23, name: 'football', description: '' },
{ tagId: 55, name: 'tournament', description: '' }]
}
How must be the query inside find function?
I was thinking of using: tags.tagId: {$in: arrayOfTagsIds} but I don't think that is going to work (it doesn't sound okay to use tags.tagId, because tags is an array)
Event
.find({tags.tagId: {$in: arrayOfTagsIds}})
.exec(function (err, events) { ... }
Also, doing this kind of query is too slow?
Yes, you can use dot notation in keys for both embedded sub-documents and arrays. However, you do need to quote your key because it contains a dot:
Event
.find({'tags.tagId': {$in: arrayOfTagsIds}})
.exec(function (err, events) { ... }
To speed up this query you could create an index on 'tags.tagId'.
Is there a way to update values in an object?
{
_id: 1,
name: 'John Smith',
items: [{
id: 1,
name: 'item 1',
value: 'one'
},{
id: 2,
name: 'item 2',
value: 'two'
}]
}
Lets say I want to update the name and value items for item where id = 2;
I have tried the following w/ mongoose:
var update = {name: 'updated item2', value: 'two updated'};
Person.update({'items.id': 2}, {'$set': {'items.$': update}}, function(err) { ...
Problem with this approach is that it updates/sets the entire object, therefore in this case I lose the id field.
Is there a better way in mongoose to set certain values in an array but leave other values alone?
I have also queried for just the Person:
Person.find({...}, function(err, person) {
person.items ..... // I might be able to search through all the items here and find item with id 2 then update the values I want and call person.save().
});
You're close; you should use dot notation in your use of the $ update operator to do that:
Person.update({'items.id': 2}, {'$set': {
'items.$.name': 'updated item2',
'items.$.value': 'two updated'
}}, function(err) { ...
model.update(
{ _id: 1, "items.id": "2" },
{
$set: {
"items.$.name": "yourValue",
"items.$.value": "yourvalue",
}
}
)
MongoDB Document
There is a mongoose way for doing it.
const itemId = 2;
const query = {
item._id: itemId
};
Person.findOne(query).then(doc => {
item = doc.items.id(itemId );
item["name"] = "new name";
item["value"] = "new value";
doc.save();
//sent respnse to client
}).catch(err => {
console.log('Oh! Dark')
});
There is one thing to remember, when you are searching the object in array on the basis of more than one condition then use $elemMatch
Person.update(
{
_id: 5,
grades: { $elemMatch: { grade: { $lte: 90 }, mean: { $gt: 80 } } }
},
{ $set: { "grades.$.std" : 6 } }
)
here is the docs
For each document, the update operator $set can set multiple values, so rather than replacing the entire object in the items array, you can set the name and value fields of the object individually.
{'$set': {'items.$.name': update.name , 'items.$.value': update.value}}
Below is an example of how to update the value in the array of objects more dynamically.
Person.findOneAndUpdate({_id: id},
{
"$set": {[`items.$[outer].${propertyName}`]: value}
},
{
"arrayFilters": [{ "outer.id": itemId }]
},
function(err, response) {
...
})
Note that by doing it that way, you would be able to update even deeper levels of the nested array by adding additional arrayFilters and positional operator like so:
"$set": {[`items.$[outer].innerItems.$[inner].${propertyName}`]: value}
"arrayFilters":[{ "outer.id": itemId },{ "inner.id": innerItemId }]
More usage can be found in the official docs.
cleaner solution using findOneAndUpdate
await Person.findOneAndUpdate(
{ _id: id, 'items.id': 2 },
{
$set: {
'items.$.name': 'updated item2',
'items.$.value': 'two updated',
}
},
);
In Mongoose, we can update array value using $set inside dot(.) notation to specific value in following way
db.collection.update({"_id": args._id, "viewData._id": widgetId}, {$set: {"viewData.$.widgetData": widgetDoc.widgetData}})
Having tried other solutions which worked fine, but the pitfall of their answers is that only fields already existing would update adding upsert to it would do nothing, so I came up with this.
Person.update({'items.id': 2}, {$set: {
'items': { "item1", "item2", "item3", "item4" } }, {upsert:
true })
I had similar issues. Here is the cleanest way to do it.
const personQuery = {
_id: 1
}
const itemID = 2;
Person.findOne(personQuery).then(item => {
const audioIndex = item.items.map(item => item.id).indexOf(itemID);
item.items[audioIndex].name = 'Name value';
item.save();
});
Found this solution using dot-object and it helped me.
import dot from "dot-object";
const user = await User.findByIdAndUpdate(id, { ...dot.dot(req.body) });
I needed to update an array element with dynamic key-value pairs.
By mapping the update object to new keys containing the $ update operator, I am no longer bound to know the updated keys of the array element and instead assemble a new update object on the fly.
update = {
name: "Andy",
newKey: "new value"
}
new_update = Object.fromEntries(
Object.entries(update).map(
([k, v], i) => ["my_array.$." + k, v]
)
)
console.log({
"$set": new_update
})
In mongoose we can update, like simple array
user.updateInfoByIndex(0,"test")
User.methods.updateInfoByIndex = function(index, info) ={
this.arrayField[index]=info
this.save()
}
update(
{_id: 1, 'items.id': 2},
{'$set': {'items.$[]': update}},
{new: true})
Here is the doc about $[]: https://docs.mongodb.com/manual/reference/operator/update/positional-all/#up.S[]
I can't seem to get the following query to work:
Group.find({ $or: [ {'groupOwner': req.user._id }, { 'subscribers.user': req.user._id, 'subscribers.level': 'owner' } ] }, 'groupName', { sort: ['groupName', 'ascending'] }, function(err, groups) {
My schema has a Group, with subscribers as a subdocument array. I want to find all groups where the groupOwner matches the passed user ID (works fine), and all documents where subscribers.user = req.user._id AND subscribers.level = 'owner'
The query as written returns all subdocuments where subscribers.user = req.user._id OR there is some subscriber with level = 'owner'. To be clear, I only want groups where subdocument has subscribers.user = req.user._id AND THE SAME SUB-DOCUMENT has subscribers.level = 'owner'.
I've tried all manner of $and and $elemMatch and just can't get it. Thanks for any help!
$elemMatch is the right operator when matching multiple fields in the same array element; your query should look like:
Group.find(
{ $or: [
{ groupOwner: req.user._id },
{ subscribers: { $elemMatch: { user: req.user._id, level: 'owner' }}}
]},
'groupName',
{ sort: ['groupName', 'ascending'] },
function(err, groups) { ...