Trouble updating a specific subdocument with MongoDB - mongodb

My document looks like:
{
_id: ObjectId("52d317d7b5c4960000587cd4"),
txid: "7e621eeb02874ab039a8566fd36f4591e65eca65313875221842c53de6907d6c",
vin: [
{
_id: ObjectId("52d317d7b5c4960000587ce9"),
meta_address: "321",
meta_amount: 50,
sequence: 4294967295,
txid: "6749762ae220c10705556799dcec9bb6a54a7b881eb4b961323a3363b00db518",
vout: 0
},
{
_id: ObjectId("52d317d7b5c4960000587ce8"),
sequence: 4294967295,
txid: "c04c413576307737f3ad48efe5d509ebc883e1d04822b3a2eccf6a80a4482932",
vout: 0
},
{
txid: "72d4fc43ac576a4b2f1f35e1b310a2d83a1012a36fdc7813ec237646950233cf",
vout: 0,
sequence: 4294967295,
_id: ObjectId("52d317d7b5c4960000587ce7")
}
]
}
My Query is:
{ txid: '7e621eeb02874ab039a8566fd36f4591e65eca65313875221842c53de6907d6c',
'vin.txid': 'c04c413576307737f3ad48efe5d509ebc883e1d04822b3a2eccf6a80a4482932',
'vin.vout': 0 }
and the update is:
{ 'vin.$.meta_address': '321',
'vin.$.meta_amount': 50 }
But when I run it, it updates the first item in the vin array instead of the second. Now, oddly enough, if I change the query to:
{ txid: '7e621eeb02874ab039a8566fd36f4591e65eca65313875221842c53de6907d6c',
'vin.txid': 'c04c413576307737f3ad48efe5d509ebc883e1d04822b3a2eccf6a80a4482932'}
then it works fine. I think the problem is that my query looks for 2 elements in the vin, but I need to search by both. What am I doing wrong?

To get the $ in your update to identify the element that matches both properties in your query, you need to use $elemMatch in your query object:
{ txid: '7e621eeb02874ab039a8566fd36f4591e65eca65313875221842c53de6907d6c',
vin: {$elemMatch: {
txid: 'c04c413576307737f3ad48efe5d509ebc883e1d04822b3a2eccf6a80a4482932',
vout: 0
}}}

Related

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
}
}
])

Can't remove object in array using Mongoose

This has been extensively covered here, but none of the solutions seems to be working for me. I'm attempting to remove an object from an array using that object's id. Currently, my Schema is:
const scheduleSchema = new Schema({
//unrelated
_id: ObjectId
shifts: [
{
_id: Types.ObjectId,
name: String,
shift_start: Date,
shift_end: Date,
},
],
});
I've tried almost every variation of something like this:
.findOneAndUpdate(
{ _id: req.params.id },
{
$pull: {
shifts: { _id: new Types.ObjectId(req.params.id) },
},
}
);
Database:
Database Format
Within these variations, the usual response I've gotten has been either an empty array or null.
I was able slightly find a way around this and accomplish the deletion by utilizing the main _id of the Schema (instead of the nested one:
.findOneAndUpdate(
{ _id: <main _id> },
{ $pull: { shifts: { _id: new Types.ObjectId(<nested _id>) } } },
{ new: true }
);
But I was hoping to figure out a way to do this by just using the nested _id. Any suggestions?
The problem you are having currently is you are using the same _id.
Using mongo, update method allows three objects: query, update and options.
query object is the object into collection which will be updated.
update is the action to do into the object (add, change value...).
options different options to add.
Then, assuming you have this collection:
[
{
"_id": 1,
"shifts": [
{
"_id": 2
},
{
"_id": 3
}
]
}
]
If you try to look for a document which _id is 2, obviously response will be empty (example).
Then, if none document has been found, none document will be updated.
What happens if we look for a document using shifts._id:2?
This tells mongo "search a document where shifts field has an object with _id equals to 2". This query works ok (example) but be careful, this returns the WHOLE document, not only the array which match the _id.
This not return:
[
{
"_id": 1,
"shifts": [
{
"_id": 2
}
]
}
]
Using this query mongo returns the ENTIRE document where exists a field called shifts that contains an object with an _id with value 2. This also include the whole array.
So, with tat, you know why find object works. Now adding this to an update query you can create the query:
This one to remove all shifts._id which are equal to 2.
db.collection.update({
"shifts._id": 2
},
{
$pull: {
shifts: {
_id: 2
}
}
})
Example
Or this one to remove shifts._id if parent _id is equal to 1
db.collection.update({
"_id": 1
},
{
$pull: {
shifts: {
_id: 2
}
}
})
Example

MongoDB not using wildcard nested array index

I have the following collection:
{
_id: 12345,
quizzes: [
{
_id: 111111,
questions: []
}
]
},
{
_id: 78910,
quizzes: [
{
_id: 22222
}
]
}
I want to select the documents of a certain quiz from the quizzes that do not have the questions array and want to make sure that it uses the appropriate questions index. So I use the following query:
Answer.find({ 'quizzes.0.questions': { $exists: false } }).explain('queryPlanner');
Which returns:
{
queryPlanner: {
plannerVersion: 1,
namespace: 'iquiz.answers',
indexFilterSet: false,
parsedQuery: { 'quizzes.0.questions': [Object] },
winningPlan: { stage: 'COLLSCAN', filter: [Object], direction: 'forward' },
rejectedPlans: []
}
}
The query is not using any index as seen from the output. I have tried the following indexes and none get used:
{ quizzes.$**: 1 }
{ quizzes.questions: 1 }
{ quizzes.[$**].questions: 1 }
{ quizzes: 1 }
The only 1 that actually gets used:
{ quizzes.0.questions: 1 }
However this is not really practical as I may target any quiz from the quizzes array not just the first one. Is there a certain syntax for the index in my case or this is a current limitation of mongodb? Thanks!
Indexes generally do not help to answer queries in the form of "X does not exist". See also mongodb indexes covering missing values.
To verify whether the index is used, look up some data using a positive condition.

mongodb: add an attribute to a subdocument

Given a collection of Users:
db.users.insertMany(
[
{
_id: 1,
name: "sue",
points: [
{ points: 85, bonus: 20 },
{ points: 85, bonus: 10 }
]
},
{
_id: 2,
name: "bob",
points: [
{ points: 85, bonus: 20 },
{ points: 64, bonus: 12 }
]
}]);
How do I add an attribute bonus_raw in every points, with a copy of the value of bonus value? I tried:
db.getCollection('users').update({ },
{$set:{ 'points.$.bonus_raw' : 'points.$.bonus' }}, false, true)
but I get:
The positional operator did not find the match needed from the query. Unexpanded update: points.$.bonus_raw
Updating multiple items in an array is not possible as of now in MongoDB.
To get this done, you will have to query the document, loop over all of your nested documents, and then save it back to MongoDB.
In your case, this can help:-
db.users.find({points: { $exists: true } }).forEach(function (doc){
doc.points.forEach(function (points) {
points.bonus_raw = points.bonus;
});
db.users.save(doc)
});
Also, take care of race conditions while doing an update in this way. See this

MongoDB update outter and array-embedded document atomically

I have an object in the cart collection:
{
_id: 1,
purchased: 0,
items: [
{
item_id: 5,
count: 0
},
{
item_id: 6,
count: 0
},
]
}
And I would like to atomically increment both purchased and count of item_id: 6. I believe the correct update command is:
db.carts.update({_id: 1, "items.item_id": 6},
{
$inc: {
purchased: 1
"items.$.count": 1
}
})
This seems to work in the console but I am not sure if this is 100% correct. Can anyone spot any issues with this update command or confirm this is correct for my use case?
For scaling this to my app, suppose _id is an ObjectId and item_id is also ObjectId (ie they are unique).