Related
I have a collection of users that has an array of multiple plates :
users : [
{
_id: '61234XXX'
plates: [
{
_id: '123'
'color': 'orange'
},
{
_id: '124'
'color': 'blue'
}
]
},
{
_id: '63456XXX'
plates: [
{
_id: '321'
'color': 'orange'
},
{
_id: '432'
'color': 'green'
}
]
}
]
I'm trying to figure out a way to add a new field to the all current plate objects for every user:
I've got to this:
await User.findOneAndUpdate(
{ _id: '63456XXX' },
{
$set : {
[`plates.0.plateStyle`]: "testValue"
}
}
)
Tho this works it's not fit for purpose. Is there a better way I can iterate over this?
You can try this query:
Here you are using array filters to tell mongo: "Where you find an object with _id: "61234XXX" and exists the plates then, there, get the document (elem) which match that plates is not empty (to get all) and add plateStyle".
await User.updateMany({
_id: "61234XXX",
plates: {
$exists: true
}
},
{
$set: {
"plates.$[elem].plateStyle": "testValue"
}
},
{
arrayFilters: [
{
elem: { $ne: [ "$plates", [] ] }
}
]
})
Example here
Also if you are finding by _id you won't need updateMany since _id is unique and only will be 1 or 0 results.
I have 2 collections, users and tracks. When I fetch the user profile I want to get his tracks.
The user object has tracks as an array of track IDs, and the second collection is of the tracks.
Now I am running this code below:
users
.aggregate([
{
$match: { _id: ObjectId(userId) },
},
{
$lookup: {
from: "tracks",
localField: "tracks",
foreignField: "_id",
as: "tracks",
},
},
{
$unwind: {
path: "$tracks",
preserveNullAndEmptyArrays: true,
},
},
{
$group: {
_id: ObjectId(userId),
username: { $first: "$username" },
profileImg: { $first: "$profileImg" },
socialLinks: { $first: "$socialLinks" },
tracks: {
$push: {
_id: "$tracks._id",
name: "$tracks.name",
categoryId: "$tracks.categoryId",
links: "$tracks.links",
mediaId: isLogged ? "$tracks.mediaId" : null,
thumbnailId: "$tracks.thumbnailId",
views: { $size: { $ifNull: ["$tracks.views", []] } },
downloads: { $size: { $ifNull: ["$tracks.downloads", []] } },
uploadedDate: "$tracks.uploadedDate",
},
},
},
},
])
In case the user does not have tracks or there are no tracks, the $ifNull statement returns an object only with these fields so it looks like that:
user: {
// user data
tracks: [{ views: 0, downloads: 0, mediaId: null }]
}
There are no tracks found so "tracks.views" cannot be read so I added the $ifNull statement, how can I avoid it from returning an empty data? also, the API call knows whether the user is logged or not (isLogged), I set the mediaId to null.
If there are no tracks found, why does the code still add these 3 fields only? no tracks to go through them...
Edit:
Any track has downloads and views containing user IDs whom downloaded / viewed the track, the track looks like that
{
"name": "New Track #2227",
"categoryId": "61695d57893f048528d049e5",
"links": {
"youtube": "https://www.youtube.com",
"soundcloud": null,
"spotify": null
},
"_id": "616c90651ab67bbd0b0a1172",
"creatorId": "61695b5986ed44e5c1e1d29d",
"mediaId": "616c90651ab67bbd0b0a1171",
"thumbnailId": "616c90651ab67bbd0b0a1170",
"plays": [],
"status": "pending",
"downloads": [],
"uploadedDate": 1634504805
}
When the fetched user doesn't have any track post yet, I receive an array with one object that contains the fields mentioned above.
What I expect to get when the user has no tracks is an empty array, as you can see the response above, the track object isn't full and contains only the conditional keys with zero value and null.
Bottom line I want the length of views and downloads only if there is a track or more, also for the mediaId which I want to hide it in case the user isn't logged. If there are no tracks I don't understand why it returns these 3 fields
expected result when the user has one track or more
user: {
// user data
tracks: [
{
name: "New Track #2227",
categoryId: "61695d57893f048528d049e5",
links: {
youtube: "https://www.youtube.com",
soundcloud: null,
spotify: null,
},
_id: "616c90651ab67bbd0b0a1172",
creatorId: "61695b5986ed44e5c1e1d29d",
mediaId: "616c90651ab67bbd0b0a1171",
thumbnailId: "616c90651ab67bbd0b0a1170",
plays: 0,
status: "pending",
downloads: 0,
uploadedDate: 1634504805,
},
];
}
expected result when the user has no tracks
user: {
// user data
tracks: [];
}
Append this stage to your pipeline:
{
$set: {
tracks: {
$filter: {
input: "$tracks",
cond: { $ne: [ { $type: "$$this._id" }, "missing" ] }
}
}
}
}
You could also modify your query to look something like this: (check out a live demo here)
This query uses a conditional push via $cond coupled with the $and operator to search for more than one condition. If tracks.downloads or tracks.plays are not greater than 0, we use the $$REMOVE variable (which just ignores that document and returns an empty array, like you are looking for).
Query
db.users.aggregate([
{
$match: {
_id: ObjectId("616c80793235ab5cc26dbaff")
},
},
{
$lookup: {
from: "tracks",
localField: "tracks",
foreignField: "_id",
as: "tracks"
}
},
{
$unwind: {
path: "$tracks",
preserveNullAndEmptyArrays: true
}
},
{
$group: {
_id: ObjectId("616c80793235ab5cc26dbaff"),
username: {
$first: "$username"
},
profileImg: {
$first: "$profileImg"
},
socialLinks: {
$first: "$socialLinks"
},
tracks: {
$push: {
// "IF" plays and downloads > 0
$cond: [
{
$and: [
{
$gt: [
"$tracks.plays",
0
]
},
{
$gt: [
"$tracks.downloads",
0
]
},
]
},
// "THEN" return document
{
_id: "$tracks._id",
name: "$tracks.name",
categoryId: "$tracks.categoryId",
links: "$tracks.links",
mediaId: "$tracks.mediaId",
thumbnailId: "$tracks.thumbnailId",
plays2: {},
plays: "$tracks.plays",
downloads: "$tracks.downloads",
uploadedDate: "$tracks.uploadedDate"
},
// "ELSE" remove
"$$REMOVE"
]
}
}
}
}
])
I have array of objects like shopping list .for example i added 2 product to my list
cart:[{id:1,quantity:15},{id:2,quantity:5}]
i can increment and decrement quantities well , but what i want to do is when i decrement to 0 i want it to show it as i dont have in my cart, i know its possible for pull and i tried this and fail.idk what i am doing wrong , when i try console.log(user) it gives me null ,because it doesnt see "cart.quantity":0 , idk why not. User info and layout is ok btw.
router.get("/:productId/decrement", auth, async (req, res) => {
const user = await User.findOne({
id: req.user._id,
"cart.quantity": 0,
});
if (user) {
await User.findOneAndUpdate(
{
id: req.user._id,
"cart.id": req.params.productId,
"cart.quantity": 0,
},
{ $pull: { cart: { id: req.params.productId } } }
);
} else {
await User.findOneAndUpdate(
{
id: req.user._id,
"cart.id": req.params.productId,
},
{ $inc: { "cart.$.quantity": -1 } },
{ new: true },
(err, doc) => {
if (err) {
return res.json({ success: false, err });
}
return res.json({ success: true, cart: doc });
}
);
}
});
and here is my user modal
const userSchema = mongoose.Schema({
name: {
type: String,
maxlength: 50,
},
email: {
type: String,
trim: true,
unique: 1,
},
password: {
type: String,
minglength: 5,
},
lastname: {
type: String,
maxlength: 50,
},
cart: { type: Array, default: [] },
On MongoDB version >= 3.2 :
router.get("/:productId/decrement", auth, async (req, res) => {
try {
let bulkArr = [
{
updateOne: {
filter: {
id: req.user._id,
"cart.id": req.params.productId,
"cart.quantity": 1, // You can use {$lte : 1}
},
update: { $pull: { 'cart': { id: req.params.productId } } },
},
},
{
updateOne: {
filter: {
id: req.user._id,
"cart.id": req.params.productId
},
update: { $inc: { "cart.$.quantity": -1 } },
},
},
];
await User.bulkWrite(bulkArr);
return res.json({ success: true });
} catch (error) {
console.log("Error ::", error);
return res.json({ success: false, ...{error} });
}
});
Note :
I believe you're mixing async-await's & callbacks(). Please try to avoid it.
In bulkWrite all filter conditions will be executed but update operation will only be executed if respective filter matches (kind of individual ops) but in one DB call.
bulkWrite outputs 'write result' but if you need result doc you need to do .find() call. But as the whole point of using bulkWrite is to avoid multiple calls to DB - If result has no errors you can send success message to front-end then they can show appropriate count (by doing decrement).
Ref : .bulkwrite()
On MongoDB version >= 4.2 :
As we can use aggregation in updates you can do like below :
User.findOneAndUpdate({id: req.user._id, 'cart.id': req.params.productId},
[
{
$addFields: {
cart: {
$reduce: {
input: "$cart",
initialValue: [],
in: {
$cond: [
{ $and: [ { $eq: [ "$$this.id", req.params.productId ] }, { $gt: [ "$$this.quantity", 1 ] } ] }, // condition
{
$concatArrays: [ "$$value", [ { $mergeObjects: [ "$$this", { quantity: { $add: [ "$$this.quantity", -1 ] } } ] } ]]
}, // condition is true, So we're decrementing & pushing object to accumulator array
{
$cond: [ { $eq: [ "$$this.id", req.params.productId ] }, "$$value", { $concatArrays: [ "$$value", [ "$$this" ] ] } ]
} // condition failed, so pushing objects where id != input productId
]
}
}
}
}
}
], {new: true})
Ref : aggregation-pipeline-for-updates, $reduce
Test aggregation pipeline here : mongoplayground
Note :
Few MongoDB clients may throw an error saying 'update operations may only contain atomic operators' that case you can use .update() or testing it using code as most drivers that support 4.2 doesn't throw such errors.
Try this:
Create a partial index that only indexes documents where the field value equals or is less than 0 (depending on your needs)
Add a TTL to "expire after 0 seconds" on a field that is set to 0. (i.e. all indexed documents are set to expire immediately).
On Java it would look like:
new IndexModel(
Indexes.ascending(ZERO_FIELD),
new IndexOptions().expireAfter(0L, TimeUnit.SECONDS)
.partialFilterExpression(Filters.lte(VALUE_FIELD, 0)
)
)
I'm trying to build some advanced hello world app on top of express and mongoose. Assume I have next Schemas:
const pollOptionsSchema = new Schema({
name: String,
votes: {
type: Number,
default: 0
}
});
const pollSchema = new Schema({
name: String,
dateCreated: { type: Date, default: Date.now },
author: { type: Schema.Types.ObjectId },
options: [pollOptionsSchema]
});
And when I simply call
Poll.findOne({_id: req.params.id}).exec((err, data) => {
if (err) console.log(err);
// I receive next data:
// { _id: 58ef3d2c526ced15688bd1ea,
// name: 'Question',
// author: 58dcdadfaea29624982e2fc6,
// __v: 0,
// options:
// [ { name: 'stack', _id: 58ef3d2c526ced15688bd1ec, votes: 5 },
// { name: 'overflow', _id: 58ef3d2c526ced15688bd1eb, votes: 3 } ],
// dateCreated: 2017-04-13T08:56:12.044Z }
});
The question is how I could receive same data + aggregated number of votes (i.e 8 in case above) after calling some method on Model level, for example:
// I want to receive:
// { _id: 58ef3d2c526ced15688bd1ea,
// name: 'Question',
// author: 58dcdadfaea29624982e2fc6,
// __v: 0,
// totalNumberOfVotes: 8,
// options:
// [ { name: 'stack', _id: 58ef3d2c526ced15688bd1ec, votes: 5 },
// { name: 'overflow', _id: 58ef3d2c526ced15688bd1eb, votes: 3 } ],
// dateCreated: 2017-04-13T08:56:12.044Z }
Or maybe I need to implement some extra method on document level i.e (data.aggregate)?
I've already reviewed:
http://mongoosejs.com/docs/api.html#model_Model.mapReduce
http://mongoosejs.com/docs/api.html#aggregate_Aggregate
https://docs.mongodb.com/manual/core/map-reduce/
https://docs.mongodb.com/manual/tutorial/map-reduce-examples/
But can't utilize it for my case :(
Any advice will be much appreciated. Thanks!
Use $reduce operator within an $addFields pipeline to create the totalNumberOfVotes field. In your aggregate pipeline, the first step is the $match which filters the document stream to allow only matching documents to pass unmodified into the next pipeline stage and uses standard MongoDB queries.
Consider running the following aggregate operation to get the desired result:
Poll.aggregate([
{ "$match": { "_id": mongoose.Types.ObjectId(req.params.id) } },
{
"$addFields": {
"totalNumberOfVotes": {
"$reduce": {
"input": "$options",
"initialValue": 0,
"in": { "$add" : ["$$value", "$$this.votes"] }
}
}
}
}
]).exec((err, data) => {
if (err) console.log(err);
console.log(data);
});
NB: The above will work for MongoDB 3.4 and greater.
For other earlier versions you would need to $unwind the options array first before grouping the denormalised documents within a $group pipeline step and aggregating with the accumulators $sum, $push and $first.
The following example shows this approach:
Poll.aggregate([
{ "$match": { "_id": mongoose.Types.ObjectId(req.params.id) } },
{ "$unwind": { "path": "$options", "preserveNullAndEmptyArrays": true } },
{
"$group": {
"_id": "$_id",
"totalNumberOfVotes": { "$sum": "$options.votes" },
"options": { "$push": "$options" },
"name": { "$first": "$name" },
"dateCreated": { "$first": "$dateCreated" },
"author": { "$first": "$author" }
}
}
]).exec((err, data) => {
if (err) console.log(err);
console.log(data);
});
[
{
"_id":"56569bff5fa4f203c503c792",
"Status":{
"StatusID":2,
"StatusObjID":"56559aad5fa4f21ca8492277",
"StatusValue":"Closed"
},
"OwnerPractice":{
"PracticeObjID":"56559aad5fa4f21ca8492291",
"PracticeValue":"CCC",
"PracticeID":3
},
"Name":"AA"
},
{
"_id":"56569bff5fa4f203c503c792",
"Status":{
"StatusID":2,
"StatusObjID":"56559aad5fa4f21ca8492277",
"StatusValue":"Open"
},
"OwnerPractice":{
"PracticeObjID":"56559aad5fa4f21ca8492292",
"PracticeValue":"K12",
"PracticeID":2
},
"Name":"BB"
}
]
In above json response,
How to group by PracticeValue,StatusValue into single function,
the below code to be used to group only StatusValue,please help how to group Practice value with the same function,
Opp.aggregate([
{$group: {
_id: '$Status.StatusValue',
count: {$sum: 1}
}}
], function (err, result) {
res.send(result);
});
and my response is,
[
{
"_id":"Deleted",
"count":0
},
{
"_id":"Open",
"count":1
},
{
"_id":"Closed",
"count":1
}
]
please help me, how to use more then $group function..
You can group by multiple fields like this:
var resultAggr = {Status: [], Practice: []};
Opp.aggregate(
[
{$group: { _id: '$Status.StatusValue', count: {$sum: 1} }}
], function (err, statusResult) {
resultAggr.Status = statusResult;
Opp.aggregate(
[
{$group: { _id: '$OwnerPractice.PracticeValue', count: {$sum: 1} }}
], function (err, practiceResult) {
resultAggr.Practice = practiceResult;
res.send([resultAggr])
});
});