consider the following document skeleton
{
_id: "615749dce3438547adfff9bc",
items: [
{
type: "shirt",
color: "red",
sizes: [
{
label: "medium",
stock: 10,
price: 20,
},
{
label: "large",
stock: 30,
price: 40,
}
]
},
{
type: "shirt",
color: "green",
sizes: [
{
label: "small",
stock: 5,
price: 3,
},
{
label: "medium",
stock: 5,
price: 3,
},
]
}
]
}
when a new item comes in, I want to insert a new document to items, unless an item exists with the same type and color as the new one, in this case I want only to merge sizes into that existing item's sizes.
sizes does not have to be unique.
I tried to use $push with upsert: true and arrayFilters but apparently $push ignores arrayFilters.
node with mongodb package.
Query1
filter to see if exists
if exists map to update, else add in the end
*2 array reads, but stil faster than query2
Test code here
db.collection.update({},
[
{
"$set": {
"newitem": {
"type": "shirt",
"color": "red",
"sizes": [
{
"label": "medium"
}
]
}
}
},
{
"$set": {
"found": {
"$ne": [
{
"$filter": {
"input": "$items",
"cond": {
"$and": [
{
"$eq": [
"$$this.type",
"$newitem.type"
]
},
{
"$eq": [
"$$this.color",
"$newitem.color"
]
}
]
}
}
},
[]
]
}
}
},
{
"$set": {
"items": {
"$cond": [
{
"$not": [
"$found"
]
},
{
"$concatArrays": [
"$items",
[
"$newitem"
]
]
},
{
"$map": {
"input": "$items",
"in": {
"$cond": [
{
"$and": [
{
"$eq": [
"$$this.type",
"$newitem.type"
]
},
{
"$eq": [
"$$this.color",
"$newitem.color"
]
}
]
},
{
"$mergeObjects": [
"$$this",
{
"sizes": {
"$concatArrays": [
"$$this.sizes",
"$newitem.sizes"
]
}
}
]
},
"$$this"
]
}
}
}
]
}
}
},
{
"$unset": [
"found",
"newitem"
]
}
])
Query2
(alternative solution)
reduce and do the update
if found keep the updated, else add in the end
*1 array read (but concat is slow, for big arrays, >500 members, if you have big arrays use query1)
*this is the normal way to do it, if we had a fast way to add in the end of the array, but we dont, so Query1 is faster
Test code here
db.collection.update({},
[
{
"$set": {
"newitem": {
"type": "shirt",
"color": "red",
"sizes": [
{
"label": "medium"
}
]
}
}
},
{
"$set": {
"items-found": {
"$reduce": {
"input": "$items",
"initialValue": {
"items": [],
"found": null
},
"in": {
"$cond": [
{
"$and": [
{
"$eq": [
"$$value.found",
null
]
},
{
"$eq": [
"$$this.type",
"$newitem.type"
]
},
{
"$eq": [
"$$this.color",
"$newitem.color"
]
}
]
},
{
"items": {
"$concatArrays": [
"$$value.items",
[
{
"$mergeObjects": [
"$$this",
{
"sizes": {
"$concatArrays": [
"$$this.sizes",
"$newitem.sizes"
]
}
}
]
}
]
]
},
"found": true
},
{
"items": {
"$concatArrays": [
"$$value.items",
[
"$$this"
]
]
},
"found": "$$value.found"
}
]
}
}
}
}
},
{
"$set": {
"items": {
"$cond": [
"$items-found.found",
"$items-found.items",
{
"$concatArrays": [
"$items-found.items",
[
"$newitem"
]
]
}
]
}
}
},
{
"$unset": [
"items-found",
"newitem"
]
}
])
Related
I have table "products" in mongodb example:
{
"_id": "62ab02ebd3e608133c947798",
"status": true,
"name": "Meat",
"type": "62918ab4cab3b0249cbd2de3",
"price": 34400,
"inventory": [
{
"_id": "62af007abb78a63a44e88561",
"locator": "62933b3fe744ac34445c4fc0",
"imports": [
{
"quantity": 150,
"_id": "62aefddcd5b52c1da07521f2",
"date_manufacture": "2022-03-01T10:43:11.842Z",
"date_expiration": "2023-05-20T10:43:20.431Z"
},
{
"quantity": 200,
"_id": "62af007abb78a63a44e88563",
"date_manufacture": "2022-04-01T10:45:01.711Z",
"date_expiration": "2023-05-11T10:45:06.882Z"
}
]
},
{
"_id": "62b3c2545a78fb4414dd718f",
"locator": "62933e07c224b41fc48a1182",
"imports": [
{
"quantity": 120,
"_id": "62b3c2545a78fb4414dd7190",
"date_manufacture": "2022-03-01T01:30:07.053Z",
"date_expiration": "2023-05-01T10:43:20.431Z"
}
]
}
],
}
I want to decrease quantity in one locator by id in imports of inventory with multiple product (bulkWrite). And can I decrease quantity sort by date_expiration?
Example: when customer order product with quantity 300 and locator 62933b3fe744ac34445c4fc0, I want to product update belike:
{
...
"name": "Meat",
"price": 34400,
"inventory": [
{
"_id": "62af007abb78a63a44e88561",
"locator": "62933b3fe744ac34445c4fc0",
"imports": [
{
"quantity": 50,
"_id": "62aefddcd5b52c1da07521f2",
"date_manufacture": "2022-03-01T10:43:11.842Z",
"date_expiration": "2023-05-20T10:43:20.431Z"
}
]
},
{
"_id": "62b3c2545a78fb4414dd718f",
"locator": "62933e07c224b41fc48a1182",
"imports": [
{
"quantity": 120,
"_id": "62b3c2545a78fb4414dd7190",
"date_manufacture": "2022-03-01T01:30:07.053Z",
"date_expiration": "2023-05-01T10:43:20.431Z"
}
]
}
],
}
Thank you so much!
You should refactor your schema as nesting array as it is considered an anti-pattern and introduces unnecessary complexity to query.
One of the options:
db={
"products": [
{
"_id": "62ab02ebd3e608133c947798",
"status": true,
"name": "Meat",
"type": "62918ab4cab3b0249cbd2de3",
"price": 34400,
"inventory": [
"62af007abb78a63a44e88561",
"62b3c2545a78fb4414dd718f"
]
}
],
"inventory": [
{
"_id": "62af007abb78a63a44e88561",
"locator": "62933b3fe744ac34445c4fc0",
"imports": [
{
"quantity": 150,
"_id": "62aefddcd5b52c1da07521f2",
"date_manufacture": ISODate("2022-03-01T10:43:11.842Z"),
"date_expiration": ISODate("2023-05-20T10:43:20.431Z")
},
{
"quantity": 200,
"_id": "62af007abb78a63a44e88563",
"date_manufacture": ISODate("2022-04-01T10:45:01.711Z"),
"date_expiration": ISODate("2023-05-11T10:45:06.882Z")
}
]
},
{
"_id": "62b3c2545a78fb4414dd718f",
"locator": "62933e07c224b41fc48a1182",
"imports": [
{
"quantity": 120,
"_id": "62b3c2545a78fb4414dd7190",
"date_manufacture": ISODate("2022-03-01T01:30:07.053Z"),
"date_expiration": ISODate("2023-05-01T10:43:20.431Z")
}
]
}
]
}
You can then do something relatively simple. Use $sortArray to sort the date_expiration and start to iterate through the arrays using $reduce.
db.inventory.aggregate([
{
$match: {
locator: "62933b3fe744ac34445c4fc0"
}
},
{
"$set": {
"imports": {
$sortArray: {
input: "$imports",
sortBy: {
date_expiration: 1
}
}
}
}
},
{
$set: {
result: {
"$reduce": {
"input": "$imports",
"initialValue": {
"qtyToDecrease": 300,
"arr": []
},
"in": {
"qtyToDecrease": {
$subtract: [
"$$value.qtyToDecrease",
{
$min: [
"$$value.qtyToDecrease",
"$$this.quantity"
]
}
]
},
"arr": {
"$concatArrays": [
"$$value.arr",
[
{
"$mergeObjects": [
"$$this",
{
"quantity": {
$subtract: [
"$$this.quantity",
{
$min: [
"$$value.qtyToDecrease",
"$$this.quantity"
]
}
]
}
}
]
}
]
]
}
}
}
}
}
},
{
$set: {
imports: "$result.arr",
result: "$$REMOVE"
}
},
{
"$merge": {
"into": "inventory",
"on": "_id"
}
}
])
Mongo Playground
Here is another version that keeps your original schema. You can see it is much more complex.
I have documents in mongo db, like
doc = {
name = MyName,
tags = tag1,tag2,tag3,
...
}
When I search documents by name, I also want to get analytics of tags, for docs with that name, like
{
tag1: 7,
tag2: 5,
...
tagn: 14
}
How can I aggregate it?
The data model complicates the query somewhat and the required output format complicates it even more ... but here's one way to do it.
db.collection.aggregate([
{
"$set": {
"tags": {
"$split": ["$tags", ","]
}
}
},
{"$unwind": "$tags"},
{
"$set": {
"tags": {
"$trim": {"input": "$tags"}
}
}
},
{
"$group": {
"_id": "$tags",
"count": {"$count": {}}
}
},
{
"$sort": {"_id": 1}
},
{
"$group": {
"_id": null,
"newRoot": {
"$mergeObjects": {
"$arrayToObject": [
[
{
"$reduce": {
"input": {"$objectToArray": "$$ROOT"},
"initialValue": {},
"in": {
"$mergeObjects": [
"$$value",
{
"$switch": {
"branches": [
{
"case": {"$eq": ["$$this.k", "_id"]},
"then": {"k": "$$this.v"}
},
{
"case": {"$eq": ["$$this.k", "count"]},
"then": {"v": "$$this.v"}
}
],
"default": "$$value"
}
}
]
}
}
}
]
]
}
}
}
},
{"$replaceWith": "$newRoot"}
])
Example output:
[
{
"tag1": 2,
"tag2": 2,
"tag3": 3,
"tag5": 1,
"tag7": 1
}
]
Try it on mongoplayground.net.
Consider I have a timesheets collection like this:
[
{
_id: 1,
createdBy: "John",
duration: "00:30"
},
{
_id: 2,
createdBy: "John",
duration: "01:30"
},
{
_id: 3,
createdBy: "Mark",
duration: "00:30"
},
]
My input is an array of usernames:
["John", "Mark", "Rikio"]
I want to use mongodb aggregate to calculate the total duration of timesheets for each user in the usernames array and If there are no timesheets found, it should return duration: "00:00". For example, it should return:
[
{createdBy: "John", totalDuration: "02:00"},
{createdBy: "Mark", totalDuration: "00:30"},
{createdBy: "Rikio", totalDuration: "00:00"}
]
However, when I use $match query, if there are no timesheets it will not return anything so I don't know which user does not have the timesheets and return "00:00" for them.
I totally agree with #turivishal , but still can make it through mongo query with an ugly one.
db.collection.aggregate([
{
$match: {}
},
{
$set: {
minutes: {
$let: {
vars: {
time: {
$split: [
"$duration",
":"
]
}
},
in: {
"$add": [
{
"$multiply": [
{
$toInt: {
$first: "$$time"
}
},
60
]
},
{
$toInt: {
$last: "$$time"
}
}
]
}
}
}
}
},
{
$group: {
"_id": "$createdBy",
"totalMinutes": {
"$sum": "$minutes"
}
}
},
{
$group: {
"_id": null,
"docs": {
"$push": "$$ROOT"
}
}
},
{
$set: {
"docs": {
$map: {
"input": [
"John",
"Mark",
"Rikio"
],
"as": "name",
"in": {
$let: {
vars: {
findName: {
"$first": {
"$filter": {
"input": "$docs",
"as": "d",
"cond": {
"$eq": [
"$$d._id",
"$$name"
]
}
}
}
}
},
in: {
"$cond": {
"if": "$$findName",
"then": "$$findName",
"else": {
_id: "$$name",
totalMinutes: 0
}
}
}
}
}
}
}
}
},
{
$unwind: "$docs"
},
{
$replaceRoot: {
"newRoot": "$docs"
}
},
{
$set: {
"hours": {
$floor: {
"$divide": [
"$totalMinutes",
60
]
}
},
"minutes": {
"$mod": [
"$totalMinutes",
60
]
}
}
},
{
$set: {
"hours": {
"$cond": {
"if": {
"$lt": [
"$hours",
10
]
},
"then": {
"$concat": [
"0",
{
"$toString": "$hours"
}
]
},
"else": {
"$toString": "$hours"
}
}
},
"minutes": {
"$cond": {
"if": {
"$lt": [
"$minutes",
10
]
},
"then": {
"$concat": [
"0",
{
"$toString": "$minutes"
}
]
},
"else": {
"$toString": "$minutes"
}
}
}
}
},
{
$project: {
duration: {
"$concat": [
"$hours",
":",
"$minutes"
]
}
}
}
])
mongoplayground
Imagine the is a document like this:
{
_id: ObjectID('someIdblahbla')
users: [
{
_id: 'id1',
name: 'name1',
},
{
_id: 'id2',
name: 'name2',
},
{
_id: 'id3',
name: 'name3'
}
]
}
I have an array like this:
const newData = [
{_id: 'id1', name: 'newName1'},
{_id: 'id2', 'name': 'newName2', family:'newFamily2'}
]
what I want is to update the array in the document using the corresponding _id and using it to add/update each element.
so my end result would be like:
{
_id: ObjectID('someIdblahbla')
users: [
{
_id: 'id1',
name: 'newName1',
},
{
_id: 'id2',
name: 'newName2',
family:'newFamily2'
},
{
_id: 'id3',
name: 'name3'
}
]
}
my guess was using The filtered positional operator but I am not sure if it's the correct way to go and how to do it.
thank you for your kind tips beforehand.
There is no straight way to add/update in array, you can use update with aggregation pipeline starting from MongoDB 4.2,
First of all, you need to convert _id from string to objectId type, if you are using mongoose npm you can use mongoose.Types.ObjectId method or if you are using mongodb npm you can use ObjectId method,
let newData = [
{ _id: 'id1', name: 'newName1' },
{ _id: 'id2', 'name': 'newName2', family:'newFamily2' }
];
let newIds = [];
newData = newData.map(n => {
n._id = ObjectId(n._id); // or mongoose.Types.ObjectId(n._id)
newIds.push(n._id); // for checking conditions
return n;
});
You can put query condition, and do below operations,
$map to iterate loop of users array, check condition if user._id is in input newIds then do update operation otherwise do insert operation
update operation:
$filter to iterate loop of input newData and filter already present object from input so we can update it
$arrayElemAt to get first object from above filtered array
$mergeObjects to merge current object with above input object
insert operation:
$filter to iterate loop of newData array and return not present object means new items in array of objects
$concatArrays to concat above new and updated result array
db.collection.updateOne(
{ _id: ObjectId("someIdblahbla") },
[{
$set: {
users: {
$concatArrays: [
{
$map: {
input: "$users",
as: "u",
in: {
$cond: [
{ $in: ["$$u._id", newIds] },
{
$mergeObjects: [
"$$u",
{
$arrayElemAt: [
{
$filter: {
input: newData,
cond: { $eq: ["$$this._id", "$$u._id"] }
}
},
0
]
}
]
},
"$$u"
]
}
}
},
{
$filter: {
input: newData,
cond: { $not: { $in: ["$$this._id", "$users._id"] } }
}
}
]
}
}
}]
)
Playground
Query1 (update(merge objects) existing members, doesn't add new members)
Test code here
Replace
[{"_id": "id1","name": "newName1"},{"_id": "id2","name": "newName2","family": "newFamily2"}] with you array or the driver variable that hold the array
db.collection.update({
"_id": {
"$eq": "1"
}
},
[
{
"$addFields": {
"users": {
"$map": {
"input": "$users",
"as": "user",
"in": {
"$reduce": {
"input": [
{
"_id": "id1",
"name": "newName1"
},
{
"_id": "id2",
"name": "newName2",
"family": "newFamily2"
}
],
"initialValue": "$$user",
"in": {
"$let": {
"vars": {
"old_user": "$$value",
"new_user": "$$this"
},
"in": {
"$cond": [
{
"$eq": [
"$$old_user._id",
"$$new_user._id"
]
},
{
"$mergeObjects": [
"$$old_user",
"$$new_user"
]
},
"$$old_user"
]
}
}
}
}
}
}
}
}
}
])
Query2 (update(merge) if found, else push in the end)
Its like the above but finds the not-existing members,and push them in the end.Its a bit more slower and complicated
Test code here
Replace
[{"_id": "id1","name": "newName1"},{"_id": "id2","name": "newName2","family": "newFamily2"},{"_id": "id4","name": "newName4"}]
with your array or the driver variable that hold the array
db.collection.update({
"_id": {
"$eq": "1"
}
},
[
{
"$addFields": {
"yourarray": [
{
"_id": "id1",
"name": "newName1"
},
{
"_id": "id2",
"name": "newName2",
"family": "newFamily2"
},
{
"_id": "id4",
"name": "newName4"
}
]
}
},
{
"$addFields": {
"new-ids": {
"$setDifference": [
{
"$map": {
"input": "$yourarray",
"as": "u",
"in": "$$u._id"
}
},
{
"$map": {
"input": "$users",
"as": "u",
"in": "$$u._id"
}
}
]
}
}
},
{
"$addFields": {
"users": {
"$concatArrays": [
{
"$map": {
"input": "$users",
"as": "user",
"in": {
"$reduce": {
"input": "$yourarray",
"initialValue": "$$user",
"in": {
"$let": {
"vars": {
"old_user": "$$value",
"new_user": "$$this"
},
"in": {
"$cond": [
{
"$eq": [
"$$old_user._id",
"$$new_user._id"
]
},
{
"$mergeObjects": [
"$$old_user",
"$$new_user"
]
},
"$$old_user"
]
}
}
}
}
}
}
},
{
"$filter": {
"input": "$yourarray",
"as": "u",
"cond": {
"$in": [
"$$u._id",
"$new-ids"
]
}
}
}
]
}
}
},
{
"$unset": [
"yourarray",
"new-ids"
]
}
])
I have following documents
users = [
{
type: 'A',
name: 'anil',
logins: [
{ at: '2-3-2019', device: 'mobile' },
{ at: '3-3-2019', device: 'desktop' },
{ at: '4-3-2019', device: 'tab' },
{ at: '5-3-2019', device: 'mobile' }
]
},
{
type: 'A',
name: 'rakesh',
logins: [
{ at: '2-3-2019', device: 'desktop' },
{ at: '3-3-2019', device: 'mobile' },
{ at: '4-3-2019', device: 'desktop' },
{ at: '5-3-2019', device: 'tab' }
]
},
{
type: 'A',
name: 'rahul',
logins: [
{ at: '2-3-2019' device: 'tab' },
{ at: '3-3-2019' device: 'mobile' },
{ at: '4-3-2019' device: 'tab' },
{ at: '5-3-2019' device: 'tab' }
]
}
]
I need to calculate percentage of device used by each user which of type "A".
if we look at user anil the device usage is,
mobile: 50%
desktop: 25%
tab: 25%
the highest usage is mobile device with 50% usage, So it should consider as mobile device.
Like above the final output would be,
[
{
name: 'anil',
device: 'mobile',
logins: 50%
},
{
name: 'rakesh',
device: 'desktop',
logins: 50%
},
{
name: 'rahul',
device: 'tab',
logins: 75%
}
]
Thanks for any help.
You can use below aggregation
Here overall logic is to find the duplicates inside the array, after that you just need to do is $map over the logins array to calculate the percentage of the device using the formula
(numberOfDevices * 100) / total size of the array
It will be better if you just remove one by one stage from the below aggregation to understand it.
db.collection.aggregate([
{ "$match": { "type": "A" }},
{ "$addFields": {
"logins": {
"$arrayToObject": {
"$map": {
"input": { "$setUnion": ["$logins.device"] },
"as": "m",
"in": {
"k": "$$m",
"v": {
"$divide": [
{
"$multiply": [
{ "$size": {
"$filter": {
"input": "$logins",
"as": "d",
"cond": {
"$eq": ["$$d.device", "$$m"]
}
}
}},
100
]
},
{ "$size": "$logins" }
]
}
}
}
}
}
}}
])
MongoPlayground
[
{
"_id": ObjectId("5a934e000102030405000000"),
"logins": {
"desktop": 25,
"mobile": 50,
"tab": 25
},
"name": "anil",
"type": "A"
},
{
"_id": ObjectId("5a934e000102030405000001"),
"logins": {
"desktop": 50,
"mobile": 25,
"tab": 25
},
"name": "rakesh",
"type": "A"
},
{
"_id": ObjectId("5a934e000102030405000002"),
"logins": {
"mobile": 25,
"tab": 75
},
"name": "rahul",
"type": "A"
}
]
Exact output-> Here I have just find the $max element from the array of object after getting the percentage of all devices.
db.collection.aggregate([
{ "$match": { "type": "A" }},
{ "$addFields": {
"logins": {
"$map": {
"input": { "$setUnion": ["$logins.device"] },
"as": "m",
"in": {
"k": "$$m",
"v": {
"$divide": [
{
"$multiply": [
{ "$size": {
"$filter": {
"input": "$logins",
"as": "d",
"cond": {
"$eq": ["$$d.device", "$$m"]
}
}
}},
100
]
},
{ "$size": "$logins" }
]
}
}
}
}
}},
{ "$replaceRoot": {
"newRoot": {
"$mergeObjects": [
"$$ROOT",
{
"$arrayElemAt": [
"$logins",
{
"$indexOfArray": [
"$logins.v",
{ "$max": "$logins.v" }
]
}
]
}
]
}
}},
{ "$project": { "logins": 0 }}
])
MongoPlayground
[
{
"_id": ObjectId("5a934e000102030405000000"),
"k": "mobile",
"name": "anil",
"type": "A",
"v": 50
},
{
"_id": ObjectId("5a934e000102030405000001"),
"k": "desktop",
"name": "rakesh",
"type": "A",
"v": 50
},
{
"_id": ObjectId("5a934e000102030405000002"),
"k": "tab",
"name": "rahul",
"type": "A",
"v": 75
}
]