Multiple groups with mongodb aggregate - javascript

I'm trying to use mongoDb aggregate on this data:
"_id": ObjectId("598dbd301ab6476e5b15e05e"),
"updated_at": ISODate("2017-08-11T14:20:32.865Z"),
"created_at": ISODate("2017-08-11T14:20:32.865Z"),
"action": ObjectId("59760749a398cb323cf1c051"),
"subAction": ObjectId("5980c3807a8cb300110d87d3"),
"person": ObjectId("598dbd2f1ab6476e5b15e05b"),
"session": ObjectId("598dbd2f1ab6476e5b15e05c"),
"dateAccomplish": ISODate("2017-08-11T14:20:32Z"),
"createdBy": ObjectId("595f8426645bf5f47366fb29"),
"updatedBy": ObjectId("595f8426645bf5f47366fb29"),
What I'm trying to do is that I need to retrieve 2 groups. It has to be grouped by actions and subactions.
The output data expected looks like this:
movactions: [
{
_id: $created_at,
count: ?,
data:[
{
_id: "$action",
count: 3,
data: [
{
_id: "$subaction",
count: 2
}
]
}
]
},
]
there are many subActions that have an action, i want to aggregate each action with their children subactions listed

To group by multiple fields, $group should be applied multiple times with compound _id, gradually reducing it in each next group:
db.collection.aggregate([
{$group:{
_id:{created_at:"$created_at", action:"$action", subAction:"$subAction"},
count: {$sum:1}
}},
{$group:{
_id:{created_at:"$_id.created_at", action:"$_id.action"},
count: {$sum:1},
data: {$push:{_id: "$_id.subAction", count:"$count"}}
}},
{$group:{
_id:"$_id.created_at",
count: {$sum:1},
data: {$push:{_id: "$_id.action", count:"$count", data:"$data"}}
}},
{$project: {
_id:0,
created_at:"$_id",
count:1,
data:1
}}
]);

Related

Mongo Query - $group

Is it possible with Mongo to make this transformation (with a $group I think) ?
or should it be done with JavaScript on the client side ?
[
{
id: 1,
lib: 'x'
},
{
id: 2,
lib: 'a'
},
{
id: 1,
lib: 'b'
},
{
id: 1,
lib: 'v'
}
]
to
[
{
id: 1,
lib_1: 'x',
lib_2: 'b',
lib_3: 'v'
},
{
id: 2,
lib_1: 'a'
}
]
Query
with $group you can easily put them in an array, and i think its best to just do this
but if you want the exact output like in your expected output its more complicated, because you need to convert array to object, and add numbers in keys etc, in general in mongodb fields are not made for data, because queries become harder, fields are for the schema
Playmongo
aggregate(
[{"$group": {"_id": "$id", "libs": {"$push": "$lib"}}},
{"$set":
{"libs":
{"$map":
{"input": {"$range": [0, {"$size": "$libs"}]},
"in":
{"k": {"$concat": ["lib_", {"$toString": {"$add": ["$$this", 1]}}]},
"v": {"$arrayElemAt": ["$libs", "$$this"]}}}}}},
{"$set": {"libs": {"$arrayToObject": ["$libs"]}}},
{"$replaceRoot": {"newRoot": {"$mergeObjects": ["$libs", "$$ROOT"]}}},
{"$project": {"libs": 0}}])

MongoDB - Decrement a certain value in a nested object, but dont let it go below 0

My MongooseSchema (simplified):
_id: (ObjectId)
storage: [
{
location: String
storedFood:[
{
"name": String
"code": String
"weight": Number
}
]
}
]
I want to dec the weight, but not below 0. There is a stackoverflow answer that does this(The second answer from #rpatel). Great! The problem here is that he uses a update-pipeline WITHOUT nested documents. I didnt find a source where I could learn something about mongdob pipelines and nested object (If you have any please let me know, I really want to learn complex pipelines)
Is someone here who could adapt the following code to decrement weight,where location equals for example "Dubai" and code equals for example "38102371982" ?
Code from #rpatel:
Mongo-Playground
Example-Document:
{
"key": 1,
value: 30
}
db.collection.update({},
[
{
$set: {
"value": {
$max: [
0,
{
$subtract: [
"$value",
20
]
}
]
}
}
}
])
A ready playground.
One option is to use double $map with $cond in order to get into the double nested array item:
db.collection.update(
{storage: {$elemMatch: {location: wantedLoc}}},
[{$set: {
storage: {
$map: {
input: "$storage",
as: "st",
in: {$cond: [
{$eq: ["$$st.location", wantedLoc]},
{location: "$$st.location",
storedFood: {$map: {
input: "$$st.storedFood",
in: {$cond: [
{$eq: ["$$this.code", wantedCode]},
{$mergeObjects: [
"$$this",
{weight: {$max: [0, {$subtract: ["$$this.weight", reduceBy]}]}}
]},
"$$this"
]}
}
}
},
"$$st"
]}
}
}
}}]
)
See how it works on the playground example

Sort with mongo and return specific objectId´s first

a structure like this: // (id´s simplified)
User:
{_id : ObjectId("4"), name: "Max Sampleman"}
{_id : ObjectId("5"), name: "Jon Doe"}
Books:
{_id : ObjectId("1"), title: "MongoDB Overview", "likes": 3, contributor: ObjectId("5") }
{_id : ObjectId("2"), title: "NoSQL Overview", "likes": 2, contributor: ObjectId("4")}
{_id : ObjectId("3"), title: "Tutorials Point Overview", "likes": 6, contributor: ObjectId("5")}
Now I'm looking for a way to find the books, sort them by likes, but return the ones from user 4 first.
An approach like this doesn't seem to work:
user4 = await User.findById(4)
books.find({}).sort({contributor: user4.id, likes: -1})
It returns an error, saying:
"message": "Invalid sort value: { contributor: 4 }"
Is there a way to achieve that kind of sorting?
The values allowed for sort are asc, desc, ascending, descending, 1, and -1, so you will not be able to pass the id instead of these values.
One way to achieve your requirement would be with aggregate and $set, You can add a new field and set it to true for the particular id you need to get first and use that field in while sorting.
books.aggregate([
{ $set: { priority: { $eq: ["$contributor", user4.id] } } },
{ $sort: { priority: -1, likes: -1 } },
]);;

Counting relationships in many to many table between joined tables in Sequelize.js

I am building a project using sequelize.js that includes a Tags table and a Stories table. They have a many to many relationship, which I created in sequelize with a through table of StoryTag. This all works perfectly so far, but I want to get a list of most popluar tags, as in how many stories they are associated with in the StoryTag table, and order them by the number of stories that use this tag.
This is the MySQL syntax of what I am trying to do. This works perfectly in MySQL Workbench:
SELECT tagName, COUNT(StoryTag.TagId)
FROM Tags
LEFT JOIN StoryTag on Tags.id = StoryTag.TagId
GROUP BY Tags.tagName ORDER BY COUNT(StoryTag.TagId) DESC;
This is what works in sequelize.js. It's a raw query, which is not ideal, but since this doesn't handle any sensitive information, it's not a huge worry, just very inelegant.
//DIRECT QUERY METHOD (TEST)
app.get("/api/directags", function (req, res) {
db.sequelize.query("select tags.id, tags.TagName, COUNT(stories.id) as num_stories
from tags left join storytag on storytag.TagId = tags.id
left join stories on storytag.StoryId = stories.id
group by tags.id order by num_stories desc;", {
type: db.Sequelize.QueryTypes.SELECT
}).then(function(result) {
res.send(result);
});
});
This outputs
[
{
"id": 3,
"TagName": "fiction",
"num_stories": 3
},
{
"id": 5,
"TagName": "Nursery Rhyme",
"num_stories": 2
},
...
{
"id": 4,
"TagName": "nonfiction",
"num_stories": 0
}
]
As it should. What doesn't quite work is:
//Sequelize count tags
//Known issues: will not order by the count
//Includes a random 'storytag' many-to-many table row for some reason
app.get("/api/sequelizetags", function (req, res) {
db.Tag.findAll({
attributes: ["id","TagName"],
include: [{
model: db.Story,
attributes: [[db.sequelize.fn("COUNT", "stories.id"), "Count_Of_Stories"]],
duplicating: false
}],
group: ["id"]
}).then(function (dbExamples) {
res.send(dbExamples);
});
});
Which outputs:
[
{
"id": 1,
"TagName": "horror",
"Stories": [
{
"Count_Of_Stories": 1,
"StoryTag": {
"createdAt": "2018-11-29T21:09:46.000Z",
"updatedAt": "2018-11-29T21:09:46.000Z",
"StoryId": 1,
"TagId": 1
}
}
]
},
{
"id": 2,
"TagName": "comedy",
"Stories": []
},
{
"id": 3,
"TagName": "fiction",
"Stories": [
{
"Count_Of_Stories": 3,
"StoryTag": {
"createdAt": "2018-11-29T21:10:04.000Z",
"updatedAt": "2018-11-29T21:10:04.000Z",
"StoryId": 1,
"TagId": 3
}
}
]
},
{
"id": 4,
"TagName": "nonfiction",
"Stories": []
},
...
{
"id": 8,
"TagName": "Drama",
"Stories": [
{
"Count_Of_Stories": 1,
"StoryTag": {
"createdAt": "2018-11-30T01:13:56.000Z",
"updatedAt": "2018-11-30T01:13:56.000Z",
"StoryId": 3,
"TagId": 8
}
}
]
},
{
"id": 9,
"TagName": "Tragedy",
"Stories": []
}
]
This is not in order, and the count of stories is buried. This seems like the sort of thing that would be a common and frequent request from a database, but I am at a loss of how to do this correctly with sequelize.js.
Resources that have failed me:
Sequelize where on many-to-many join
Sequelize Many to Many Query Issue
How to query many-to-many relationship data in Sequelize
Select from many-to-many relationship sequelize
The official documentation for sequelize: http://docs.sequelizejs.com/manual/tutorial/
Some less official and more readable documentation for sequelize: https://sequelize.readthedocs.io/en/v3/docs/querying/
Here's what finally worked, in case anyone else has this question. We also added a where to the include Story, but that's optional.
This resource is easier to understand than the official sequelize docs: https://sequelize-guides.netlify.com/querying/
I also learned that being familiar with promises is really helpful when working with sequelize.
db.Tag.findAll({
group: ["Tag.id"],
includeIgnoreAttributes:false,
include: [{
model: db.Story,
where: {
isPublic: true
}
}],
attributes: [
"id",
"TagName",
[db.sequelize.fn("COUNT", db.sequelize.col("stories.id")), "num_stories"],
],
order: [[db.sequelize.fn("COUNT", db.sequelize.col("stories.id")), "DESC"]]
}).then(function(result){
return result;
});
Please, use the same name if you mean the same thing (num_stories - Count_Of_Stories, etc.).
For ordering use order option.
Include count in top level attributes for get it on top level of instance.
I can't find include[].duplicating option in doc.
Your case:
db.Tag.findAll({
attributes: [
"id",
"TagName",
[db.sequelize.fn("COUNT", "stories.id"), "Count_Of_Stories"]
],
include: [{
model: db.Story,
attributes: [],
duplicating: false
}],
group: ["id"],
order: [
[db.sequelize.literal("`Count_Of_Stories`"), "DESC"]
]
});
Use through: {attributes: []} in options

aggregation mongodb, return all subdocument after match

I have the collections books, with the following data:
books: {
title: "Mickey",
subtitle: "The Mouse",
authors: [
{
name: "Walt Disney",
type: "Author"
},
{
name: "Donald Duffy",
type: "Co-Author"
},
categories: [
"kids, education"
]
]
}
What I am trying to do is after $unwind my authors array and then $match with $regex for coincidences if the field autor.name has whatever string I am sending, for example
$match: { "author.name": {$regex: ".*walt.*", $options: "si"}, title: "Mickey"}
then after $group and $push my array, I end up with
books: {
title: "Mickey",
subtitle: "The Mouse",
authors: [
{
name: "Walt Disney",
type: "Author"
}, categories: [
"kids, education"
]
]
}
Is there a way or operator in mongodb to keep all my subdocuments after a matching a field, the reason why I want this, is because on the front-end of my app every author and category would be a link to all the books that have that name or category and I want to display all of them.
You mean you want to include “Donald Duffy” as in your initial document? If so you can use simple find by making use of the dot notation so you won’t have to use aggregation. db.collection.find({"authors.name": {$regex: ".*walt.*", $options: "si"}, "title": "Mickey"})
The same with aggregation framework:
db.collection.aggregate([
{
$match: {
"authors.name": {$regex: ".*walt.*", $options: "si"},
"title": "Mickey"
}
}
])

Categories

Resources