How to Update field with result of $multiply MongoDB - mongodb

I have a collection of documents that look like this:
{
portfolio: [
0: {
price: 27
amount: 10
balance: 0
}
1: {
price: 28
amount: 30
balance: 0
}
2: {
price: 39
amount: 20
balance: 0
}
]
}
I'm trying to update the value of balance for all elements in the array with the multiple of price and amount
This is the line of code I'm trying to execute:
collection.update({}, [{"$set": {"portfolio.$[].balance": {"$multiply": ["portfolio.$.price","portfolio.$.amount"]}}}])
I keep getting the error:
builtins.TypeError: document must be an instance of dict, bson.son.SON, or any other type that inherits from collections.Mapping
What am I doing wrong?
PS:
I'm using [] brackets around {$set} of let update() know it's an aggregation pipeline. I'm using $[] to update all values for all elements of portfolio
UPDATE:
Can complete the task with logic spelled out in python for anyone interested:
accounts = collection.find()
for account in accounts:
portfolio = account["portfolio"]
accountid = ObjectId(account["_id"])
for i, asset in enumerate(portfolio):
asset["balance"] = asset["price"] * asset["amount"]
portfolio[i] = asset
collection.update({"_id": accountid}, {"$set": {"portfolio": portfolio}})
If there is a way to do this in MongoDB, please let me know.

I think the below query is what you are looking for.
Note: This query will only work on MongoDB version >= 4.2
You can make use of the $map operator in a $projection pipeline stage and finally the $set pipeline to update the result obtained. All this is achieved since MongoDB >= 4.2 supports Aggregation Pipeline stages within its update parameter.
db.tmp1.update({},
[
{
"$project": {
"portfolio": {
"$map": {
"input": "$portfolio",
"as": "pf",
"in": {
"balance": {
"$multiply": [
"$$pf.price",
"$$pf.amount"
]
},
"price": "$$pf.price",
"amount": "$$pf.amount",
}
}
}
}
},
{
"$set": {
"portfolio": "$portfolio",
}
},
], { multi: true })
$[] will work only when you want to update a single static value to all the array elements.

Related

MongoDB, Panache, Quarkus: How to do aggregate, $sum and filter

I have a table in mongodb with sales transactions each containing a userId, a timestamp and a corresponding revenue value of the specific sales transaction.
Now, I would like to query these users and getting the minimum, maximum, sum and average of all transactions of all users. There should only be transactions between two given timestamps and it should only include users, whose sum of revenue is greater than a specified value.
I have composed the corresponding query in mongosh:
db.salestransactions.aggregate(
{
"$match": {
"timestamp": {
"$gte": new ISODate("2020-01-01T19:28:38.000Z"),
"$lte": new ISODate("2020-03-01T19:28:38.000Z")
}
}
},
{
$group: {
_id: { userId: "$userId" },
minimum: {$min: "$revenue"},
maximum: {$max: "$revenue"},
sum: {$sum: "$revenue"},
avg: {$avg: "$revenue"}
}
},
{
$match: { "sum": { $gt: 10 } }
}
]
)
This query works absolutely fine.
How do I implement this query in a PanacheMongoRepository using quarkus ?
Any ideas?
Thanks!
A bit late but you could do it something like this.
Define a repo
this code is in kotkin
class YourRepositoryReactive : ReactivePanacheMongoRepository<YourEntity>{
fun getDomainDocuments():List<YourView>{
val aggregationPipeline = mutableListOf<Bson>()
// create your each stage with Document.parse("stage_obj") and add to aggregates collections
return mongoCollection().aggregate(aggregationPipeline,YourView::class.java)
}
mongoCollection() automatically executes on your Entity
YourView, a call to map related properties part of your output. Make sure that this class has
#ProjectionFor(YourEntity.class)
annotation.
Hope this helps.

Performing sorting query in MongoDB

I want to make this complex sorting query in MongoDB but I am failing to achieve it.
The model in the collection looks like this:
_id: UUID('some-id'),
isDeleted: false,
date: ISODate('some-date'),
responses: [{
_id: UUID('some-id'),
userId: UUID('some-id'),
response: 0
}, {
_id: UUID('some-id'),
userId: UUID('some-id'),
response: 1
}]
One thing to keep in mind is that the responses array will always have 2 or 3 objects inside it. Not more, not less. Also, the response will only have three values, either 0, 1, or 2.
And what I want to do is that I want to sort them differently for each user, based on their response.
So let's say that my collection which is called Events has a lot of objects in the database. I want that when I filter them, the sorting will be done like this:
If my response is 0 and others are either 0 or 1, then sort them always first.
If all responses are 1, sort them after.
Others (if any response is 2, or if my response is 1 but others are 1 or 0), sort them last.
We can find if its my response by passing the userId in the query.
On top of that, I will need to have pagination so I will need to implement the $skip and $limit.
Was giving it a try with $unwind then $project trying to do some scoring based sorting, but couldn't achieve it.
The scoring sorting would look something like this:
if my response is 0 and others are 0 or 1 -> score = 100
if all responses are 1 -> score = 50
all others -> score = 0
In this way we could order them by score. But I dont know how I can actually create this property in the fly.
Was thinking that we could create one property like this:
$project: {
myScore: {
$cond: {
if: {
$in: [
UUID('my-user-id'),
"$responses.userId"
],
then: "$respones.response", //this is returning array here with all responses
else: 0
}
}
},
totalScore: {
$sum: "$respones.response"
}
}
And then we would be able to do another stage where we sort on these numbers somehow.
Thank you! :)
Here is a slightly simplified input set. We also include a target field for help in testing the scoring algo; it is not necessary for the final pipeline, where score is A, B, C for first, middle, and last in sort order. The score can be "anything" as long as it sorts properly. I used A, B, and C because it is visually different than the response codes (0,1,2) we are looking at so the pipeline functions are a little more comprehensible but it could be 10, 20, 30 or 5,10,15.
var myUserId = 1;
var r = [
{
target: 'C', // last, myUserId response is 1
responses: [
{userId:0, response:0},
{userId:1, response:1}
]
}
,{
target: 'C', // last, myUserId response is 1
responses: [
{userId:1, response:1},
{userId:0, response:0}
]
}
,{
target: 'A', // first, myUserId response is 0
responses: [
{userId:1, response:0},
{userId:0, response:0}
]
}
,{
target: 'B', // mid, all 1s
responses: [
{userId:7, response:1},
{userId:9, response:1}
]
}
,{
target: 'C', // last, a 2 exists
responses: [
{userId:4, response:2},
{userId:3, response:1},
{userId:1, response:0}
]
}
];
This pipeline will produce the desired output:
db.foo.aggregate([
{$addFields: {score:
{$cond: [
{$in: [2, '$responses.response']}, // any 2s?
'C', // yes, assign last
{$cond: [ // else
// All responses 1 (i.e. set diff is from 1 is empty set []?
{$eq: [ {$setDifference:['$responses.response',[1]]}, [] ] },
'B', // yes, assign mid
{$cond: [ // else
// Does myUserId have a response of 0? filter the
// array on these 2 fields and if the size of the
// filtered array != 0, that means you found one!
{$ne:[0, {$size:{$filter:{input:'$responses',
cond:{$and:[
{$eq:['$$this.userId',myUserId]},
{$eq:['$$this.response',0]}
]}
}} } ]},
'A', // yes, assign first
'C', // else last for rest
]}
]}
]}
}}
,{$sort: {'score':1}}
// TEST: Show items where target DOES NOT equal score. If the pipeline
// logic is working, this stage will produce zero output; that's
// how you know it works.
//,{$match: {$expr: {$ne:['$score','$target']}} }
]);
Anyone wondering about this, here's what I came up with. p.s. I also decided that I need to ignore all items if any response contains response 2, so I will focus only on values 0 and 1.
db.invites.aggregate([
{
$match: {
"$responses.response": {
$ne: 2
}
}
},
{
$addFields: {
"myScore": {
"$let": {
"vars": {
"invite": {
// get only object that contains my userId and get firs item from the list (as it will always be one in the list)
"$arrayElemAt": [{
"$filter": {
"input": "$responses",
"as": "item",
"cond": {"$eq": ["$$item.userId", UUID('some-id')]}
}} ,0]
}
},
// ger response value of that object that contains my userId
"in": "$$invite.response"
}
},
// as they are only 0 and 1s in the array items, we can see how many percent have voted with one.
// totalScore = sum(responses.response) / size(responses)
"totalScore": {
$divide: [{$sum: "$responses.response"} , {$size: "$responses"}]
}
}
},
{
$sort: {
//sort by my score, so if I have responded with 0, show first
"myScore": 1,
//sort by totalScore, so if I have responded 1, show those that have all 1s first.
"totalScore": -1
}
}
])

How to do fast query on String dataType in MongoDB, where values are of type double

I have a field contractValue and other fields in a collection contract which is of type String . It basically holds double value like 1200 or 1500 but at some places it may contain value like $1200 or $1500.
Sample data from collection:
{ ..
..
contractValue: "1200", //This is the one stored as String. I need
// to perform range query over it
..
..
}
{ ..
..
contractValue: "$1500",
..
..
}
I have requirement where i need to fetch contracts based on contract values. Query can be like below:
{$and: [ {'contractValue': {$gt: 100}}, {'contractValue': {$lt: 1000 }}]}
This query is giving me wrong result. It is also giving me documents having contractValue like 1238999
Also I need to create indexes on contractValue
Is it possible to create index on contract value , so that I can efficiently make range query, so that whenever making any query, it will do < or > on Index and will fetch exact set of documents, rather than making change in schema?
How to handle values like $1200 in index, so index value just contain 1200 as integer
rather than $1200
try this:
https://mongoplayground.net/p/TG3Y5tdh9aK
it assumes string data will be either a quoted number or a quoted number with "$" at the front
db.collection.aggregate([
{
$project: {
"newContractValue": {
"$convert": {
"input": "$contractValue",
"to": "double",
"onError": {
$toDouble: {
"$substr": [
"$contractValue",
1,
{
"$strLenCP": "$contractValue"
}
]
}
}
}
}
}
},
{
$match: {
$and: [
{
"newContractValue": {
$gt: 100
}
},
{
"newContractValue": {
$lt: 1000
}
}
]
}
}
])
This can be used to set a new contractValueNew field as number from the existing contractValue
db.getCollection('yourCollection').find({})
.forEach(function(record) {
if(record.contractValue.toString().substring(0, 1) == '$') {
record.contractValueNew = NumberInt(parseInt(record.contractValue.substring(1, record.contractValue.length)));
} else {
record.contractValueNew = NumberInt(parseInt(record.contractValue))
}
db.getCollection('yourCollection').save(record)
})
Try:
db.collection.find({'contractValue': {$gt: 100, $lt: 1000 }})
Create index on contractValue , but convert all values as numbers ...

MongoDB: Add field to all objects in array, based on other fields on same object?

I am fairly new to MongoDB and cant seem to find a solution to this problem.
I have a database of documents that has this structure:
{
id: 1
elements: [ {elementId: 1, nr1: 1, nr2: 3}, {elementId:2, nr1:5, nr2: 10} ]
}
I am looking for a query that can add a value nr3 which is for example nr2/nr1 to all the objects in the elements array, so that the resulting document would look like this:
{
id: 1
elements: [ {elementId: 1, nr1: 1, nr2: 3, nr3:3}, {elementId:2, nr1:5, nr2: 10, nr3: 2} ]
}
So I imagine a query along the lines of this:
db.collection.updateOne({id:1}, {$set:{"elements.$[].nr3": nr2/nr1}})
But I cant find how to get the value of nr2 and nr1 of the same object in the array.
I found some similar questions on stackoverflow stating this is not possible, but they were 5+ years old, so I thought maybe they have added support for something like this.
I realize I can achieve this with first querying the document and iterate over the elements-array doing updates along the way, but for the purpose of learning I would love to see if its possible to do this in one query.
You can use update with aggregation pipeline starting from MongoDB v4.2,
$map to iterate loop of elements
divide nr2 with nr1 using $divide
merge current object and new field nr3 using $mergeObjects
db.collection.updateOne(
{ id: 1 },
[{
$set: {
elements: {
$map: {
input: "$elements",
in: {
$mergeObjects: [
"$$this",
{ nr3: { $divide: ["$$this.nr2", "$$this.nr1"] } }
]
}
}
}
}
}]
)
Playground
db.collection.update(
{ id:1},
{ "$set": { "elements.$[elem].nr3":elements.$[elem].nr2/elements.$[elem].nr1} },
{ "multi": true }
);
I guess this should work

Display object list with a parameter on Mongoose

I have a find query that returns me a list of objects:
{
"_id": "5fb94fda487b9348c4291450",
"name": [
{
"NewConfirmed": 642686,
"TotalConfirmed": 49315431,
"NewDeaths": 9555,
"TotalDeaths": 1242785,
"NewRecovered": 288131,
"TotalRecovered": 32473892
},
{
"NewConfirmed": 116262,
"TotalConfirmed": 6014461,
"NewDeaths": 4640,
"TotalDeaths": 371913,
"NewRecovered": 77575,
"TotalRecovered": 2492884
},
{
...
Its all fine but I'm trying to make a new query with a status parameter with the value NewConfirmed or TotalConfirmed or NewDeaths to display only that specific field. So the endpoints would look like /something/status/:status.
I already tried an aggregation with filter and a simple find but still havent figured nothing out.
Anyone has any idea?
First of all, you need a query with this estructure:
db.collection.aggregate([
{
/**Your match object*/
},
{
"$project": {
"YourStatus": {
"$first": "$name.YourStatus"
}
}
}
])
Example here.
Using mongoose you need to create the object query in this way:
var query = {}
query[status] = {"$first": "$name."+status}
And do the mongoose query replacing the object by query object.
var aggregate = await model.aggregate([
{
//Your $match stage here
},
{
"$project": query
}
])
Also, I've tested in local but my mongo version (I think) doesn't recognize $first so I've used $arrayElemAt. According to mongo docs is the same as $first.
var status = "NewConfirmed"
var query = {}
query[status] = { $arrayElemAt: ["$name."+status, 0]}
Also you can add _id: 0 into $project aggregate to not return this field.
var query = {_id:0} //Here add _id: 0 to project object
query[status] = { $arrayElemAt: ["$name."+status, 0]} //And the rest of the stage