Transform field in document's array - mongodb

I'm trying to convert some fields from string to integer with aggregate pipelines in MongoDB.
These fields are nested in a document's array, so that they are handled as array too.
Below an example document:
{
"_id": *****,
"subject": "first",
"items": [
"item_id": "1",
"ratings": {
"count": 90,
"value": "8",
},
"item_id": "2",
"ratings": {
"count": 75,
"value": "9",
},
]
}
I tried with $convert and $map (on items.item_id and items.rating.value) but I got the following results:
$convert: cannot convert array to int
$map: each field becomes an array itself holding values from all items:
{
"subject": "first",
"items": [
"item_id": [1, 2],
"ratings": {
"count": 90,
"value": [8, 9],
},
"item_id": [1, 2],
"ratings": {
"count": 75,
"value": [8, 9],
},
]
}
Any help is appreciated :)

$map to iterate loop of items array,
$toInt to convert value to integer type
$mergeObjects to merge current object with modified fields value
db.collection.aggregate([
{
$addFields: {
items: {
$map: {
input: "$items",
in: {
$mergeObjects: [
"$$this",
{
"ratings": {
count: "$$this.ratings.count",
value: { $toInt: "$$this.ratings.value" }
}
}
]
}
}
}
}
}
])
Playground

Related

How to compare two fields of one subdocument in array?

Given some documents in MongoDB
[
{
"id": 1,
"items": [
{
"id": "T0001",
"x": 10,
"y": 10
},
{
"id": "T0002",
"x": 10,
"y": 5
},
{
"id": "T0003",
"x": 10,
"y": 10
},
{
"id": "T0004",
"x": 10,
"y": 20
}
]
},
{
"id": 2,
"items": [
{
"id": "T0001",
"x": 10,
"y": 5
},
{
"id": "T0002",
"x": 10,
"y": 15
}
]
}
]
I would like to remove the subdocuments with items.id="T0001" and items.id="T0002" in document id=1. This could be done with the following command.
db.collection.update({
id: 1
},
{
$pull: {
items: {
id: {
$in: [
"T0001",
"T0002"
]
}
}
}
})
However, if I would like to add one more condition "items.x === items.y" into the $pull operation. (i.e. only the subdocument whose id="T0001" will be removed because its x=10 and y=10)
The sample code can be found here https://mongoplayground.net/p/rYJp8wQPpcS.
Could anyone show me some tips to solve this problem? Thank you!
Working on the update with the aggregation pipeline will be easier.
$set - Set the items field.
1.1. $filter - Filter the items array by condition:
1.1.1. $not - Negate the result of 1.1.1.1.
1.1.1.1 $and - Fulfill both conditions of id in ["T0001", "T0002"] and x and y are equal.
db.collection.update({
id: 1
},
[
{
$set: {
items: {
$filter: {
input: "$items",
cond: {
$not: {
$and: [
{
$in: [
"$$this.id",
[
"T0001",
"T0002"
]
]
},
{
$eq: [
"$$this.x",
"$$this.y"
]
}
]
}
}
}
}
}
}
])
Sample Mongo Playground

MongoDB aggregation to change the key based on value in each object

Is there an aggregation query available to convert the following:
after unwind I plan to change the key based on the value inside each object and append it too has---(type)
{
"rows": [
{
"type": "aaa",
"values": [
1,
2,
3
]
},
{
"type": "bbb",
"values": [
4,
5,
6
]
}
]
}
to
{
"hasaaa": {
"type": "aaa",
"values": [
1,
2,
3
]
},
"hasbbb": {
"type": "bbb",
"values": [
4,
5,
6
]
}
}
$map to iterate loop of rows array
$concat to prepare the custom key string
return key-value from map
$arrayToObject convert key-value array to object
db.collection.aggregate([
{
$project: {
rows: {
$arrayToObject: {
$map: {
input: "$rows",
in: {
k: { $concat: ["has", "$$this.type"] },
v: "$$this"
}
}
}
}
}
}
])
Playground

How to add calculated fields inside subdocuments without using unwind?

I'm looking for a simple solution to add a field to a subdocument without using $unwind and $group.
I need only to calculate the sum and the size of a nested subdocuments and show it in a new field.
This is my starting collection:
{
"_id": ObjectId("5a934e000102030405000000"),
"subDoc": [
{
"a": 1,
"subArray": [1,2,3]
},
{
"b": 2
}
]
},
{
"_id": ObjectId("5a934e000102030405000001"),
"subDoc": [
{
"a": 1,
"subArray": [4,5,6]
},
{
"b": 2
},
{
"c": 3,
"subArray": [8,8,8]
}
]
}
And this is my desired result, where I've added sum (sum of subArray) and size (number of elements in subArray):
{
"_id": ObjectId("5a934e000102030405000000"),
"subDoc": [
{
"a": 1,
"subArray": [1,2,3],
"sum": 6,
"size": 3
},
{
"b": 2
"sum": 0,
"size": 0
}
]
},
{
"_id": ObjectId("5a934e000102030405000001"),
"subDoc": [
{
"a": 1,
"subArray": [4,5,6],
"sum": 15,
"size": 3
},
{
"b": 2,
"sum": 0,
"size": 0
},
{
"c": 3,
"subArray": [8,8],
"sum": 16,
"size": 2
}
]
}
I know how to obtain this result using $unwind and then $group, but I'd like to know if there is any other way (or a better way!) to achieve the same result. I've tried using $addFields and $map without success.
Working playground example: https://mongoplayground.net/p/fK8t6SLlOHa
$map to iterate loop of subDoc array
$sum to get total of subArray array of numbers
$ifNull to check if field is not present or null then return empty array because $size operator only allows array input
$size to get total elements in subArray array
$mergeObjects to merge current object with new added fields
db.collection.aggregate([
{
$addFields: {
subDoc: {
$map: {
input: "$subDoc",
in: {
$mergeObjects: [
"$$this",
{
sum: { $sum: "$$this.subArray" },
size: {
$size: { $ifNull: ["$$this.subArray", []] }
}
}
]
}
}
}
}
}
])
Playground

How can I get a single item from the array and display it as an object? and not as an array Mongodb

I have a collection from which I need specific obj e.g. notes.blok2 and notes.curse5 as an object, not as an array
{
"year":2020,
"grade":4,
"seccion":"A",
"id": 100,
"name": "pedro",
"notes":[{"curse":5,
"block":1,
"score":{ "a1": 5,"a2": 10, "a3": 15}
},{"curse":5,
"block":2,
"score":{ "b1": 10,"b2": 20, "b3": 30}
}
]
}
My query
notas.find({
"$and":[{"grade":1},{"seccion":"A"},{"year":2020}]},
{"projection":{ "grade":1, "seccion":1,"name":1,"id":1,
"notes":{"$elemMatch":{"block":2,"curse":5}},"notes.score":1} })
It works but returns notes like array
{
"_id": "55",
"id": 100,
"grade": 5,
"name": "pedro",
"seccion": "A",
"notes": [
{"score": { "b1": 10,"b2": 20, "b3": 30} }
]
}
But I NEED LIKE THIS: score at the same level as others and if doesn't exist show empty "score":{}
{
"year":2020,
"grade":5,
"seccion":"A",
"id": 100,
"name": "pedro",
"score":{ "b1": 10,"b2": 20, "b3": 30}
}
Demo - https://mongoplayground.net/p/XlJqR2DYW1X
You can use aggregation query
db.collection.aggregate([
{
$match: { // filter
"grade": 1,
"seccion": "A",
"year": 2020,
"notes": {
"$elemMatch": {
"block": 2,
"curse": 5
}
}
}
},
{ $unwind: "$notes" }, //break into individual documents
{
$match: { // match query on individual note
"notes.block": 2,
"notes.curse": 5
}
},
{
$project: { // projection
"grade": 1,
"seccion": 1,
"name": 1,
"id": 1,
"score": "$notes.score"
}
}
])
Update
Demo - https://mongoplayground.net/p/mq5Kue3UG42
Use $filter
db.collection.aggregate([
{
$match: {
"grade": 1,
"seccion": "A",
"year": 2020
}
},
{
$set: {
"score": {
"$filter": {
"input": "$notes",
"as": "note",
"cond": {
$and: [
{
$eq: [ "$$note.block",3]
},
{
$eq: [ "$$note.curse", 5 ]
}
]
}
}
}
}
},
{
$project: {
// projection
"grade": 1,
"seccion": 1,
"name": 1,
"id": 1,
"score": {
"$first": "$score.score"
}
}
}
])
If you want empty object for score when match not found you can do -
Demo - https://mongoplayground.net/p/dumax58kgrc
{
$set: {
score: {
$cond: [
{ $size: "$score" }, // check array length
{ $first: "$score" }, // true - take 1st
{ score: {} } // false - set empty object
]
}
}
},

Is it possible to make a string date comparision inside the mongo $filter cond operator?

In this case, to fetch more compact data from MongoDB, I need to filter subdocument (records) by date value that has a string type. As you can see below, the record document is a nested array.
[
{
"_id": 14,
"isActive": true,
"name": "D.HONGKONG-1",
"factory_id": 10,
"factory_name": "CHAOI",
"branches":
{
"_id": 205,
"isActive": true,
"name": "DZZ NUCE",
"region_id": 14,
"owner_name": "A",
"phone": "",
"records": [
{
"date": "24-10-2020",
"sales": [
{
"time": "17:58",
"explanation": "DAILY CALCULATION",
"type": "DAILY",
"quantity":
{
"P": 0,
"K": 0,
"U": 0
}
}],
"stocks": [
{
"time": "17:58",
"explanation": "DELIVERY COMPL",
"type": "DELIVERY",
"quantity":
{
"P": 0,
"K": 0,
"U": 0
}
},
{
"time": "17:58",
"explanation": "DAILY S. ENTRY",
"type": "DAILY",
"quantity":
{
"P": 0,
"K": 0,
"U": 0
}
}],
"delivery":
{
"P": 0,
"K": 0,
"U": 0
},
"material": []
},
{
"date": "23-10-2020",
"sales": [
{
"time": "17:58",
"explanation": "",
"type": "DAILY",
"quantity":
{
"P": 0,
"K": 0,
"U": 0
}
}],
"stocks": [
{
"time": "17:58",
"explanation": "",
"type": "DELIVERY",
"quantity":
{
"P": 0,
"K": 0,
"U": 0
}
},
{
"time": "17:58",
"explanation": "",
"type": "DAILY",
"quantity":
{
"P": 0,
"K": 0,
"U": 0
}
}],
"delivery":
{
"P": 0,
"K": 0,
"U": 0
},
"material": []
}]
}
}]
When I try to achieve this goal by using the script below, I have encountered some issues listed below.
ConversionFailure (code:241):
I think $dateFromString couldn't consume "$$record.date" filter value. It is working when I use it without $dateFromString.
LocationError (code:31261):
While using the $function to compare dates, the cond argument of $function throws an error like this. So, I couldn't use a function too.
aggregate([{
$match: {
factory_id: parseInt(factoryId),
isActive: true
}
},
{
$lookup: {
from: 'branches',
localField: '_id',
foreignField: 'region_id',
as: 'branches',
},
},
{
$unwind: {
path: '$branches'
}
},
{
$project: {
name: 1,
factory_id: 1,
factory_name: 1,
isActive: 1,
// 'order': 1,
'branches._id': 1,
'branches.name': 1,
'branches.isActive': 1,
'branches.region_id': 1,
'branches.owner_name': 1,
'branches.phone': 1,
'branches.records': {
$filter: {
input: '$branches.records',
as: 'record',
cond: {
$eq: [{
$dateFromString: {
dateString: "11-11-2021",
format: "%d-%m-%Y"
}
},
{
$dateFromString: {
dateString: "$$record.date",
format: "%d-%m-%Y"
}
}
]
}
}
}
}
}
])
I really didn't find a solution to compare these dates inside $filter cond to complete my requirement. What are the possible solutions? Thanks
I have solved my problem myself. It may be a workaround but it works now.
In the $filter operator cond attribute needs the same date object to compare. However, the parsing is impossible for $filter reference values, due to the methods weren't able to read them. So at this point, to do this, we need to use the $convert operator as below.
cond: {
$gte: [{
$dateFromString: {
dateString: lastDate.format('DD-MM-YYYY'),
format: '%d-%m-%Y',
},
},
{
$dateFromString: {
dateString: {
$convert: {
input: '$$record.date',
to: 'string',
},
},
format: '%d-%m-%Y',
},
},
]
}