Why is addToSet in mongodb not working in an array field? - node.js

The documents in the collection have an array field of sub-documents, each with a counter that should be increased up to three, but if the array doesn't have a sub-document with a given key it should create it with the default values.
The documentation for $addToSet says:
Behavior
$addToSet only ensures that there are no duplicate items added to the set and does not affect existing duplicate elements. $addToSet does not guarantee a particular ordering of elements in the modified set.
Missing Field
If you use $addToSet on a field is absent in the document to update, $addToSet creates the array field with the specified value as its element.
The problem is that the array field is not created in the document if it doesn't exist, as stated in the documentation.
This is what I'm currently using to accomplish the operation:
// increase counter in element of array if element exist and counter is less than 3
collection.updateOne({
key_1,
"array.key_2": key_2,
"array.counter": {$lt: 3}
}, {
$inc: {"array.$.counter": 1}
})
.then(res => {
console.log("!!!!1:", res.modifiedCount, res.upsertedId, res.upsertedCount, res.matchedCount);
if (res.matchedCount) return res.matchedCount;
// create element in array with the default values,
// also create the array field if it doesn't exist
collection.updateOne({
key_1
}, {
$addToSet: {
array: {key_2, counter: 1}
}
})
.then(res => {
console.log("!!!!2:", res.modifiedCount, res.upsertedId, res.upsertedCount, res.matchedCount);
return res.matchedCount;
})
.catch(e => console.log(e))
})
.catch(e => console.log(e))
Using upsert in the second query, it creates the array field if it doesn't exist but then when array.counter reaches 3 in subsequent calls to increase its value, the operation creates a new sub-document in the array with the same values for array.key_2 and array.date, effectible duplicating the entry although with array.counter set to 1 instead of 3.
I'm using mongo version 4.2.1
Update:
Below there is a sample document before trying to run the operation on the second subdocument:
{
"key_1": 1,
"array": [
{
"key_2": 1,
"counter" 1
}, {
"key_2": 2,
"counter" 3
}
]
}
This is what I'm getting as a result when using upsert:
{
"key_1": 1,
"array": [
{
"key_2": 1,
"counter" 1
}, {
"key_2": 2,
"counter" 3
}, {
"key_2": 2,
"counter" 1
}
]
}
The operation is duplicating the second subdocument in array, but if upsert is not used then the array field is not created if it's not already in the parent document, which is the oposite of the expected behavior for $addToSet from what it says in the documentation.
Update 2
These are the steps to reproduce the issue:
Run the operation with key_1 set to 1, and upsert disabled. None of the queries modifies the document. The array field is not created.
{
"key_1": 1
}
Enable upsert and run the operation again. The array field is created in the second query:
{
"key_1": 1,
"array": [
{
"key_2": 1,
"counter" 1
}
]
}
Run the operation again twice more. The first query modifies the document twice:
{
"key_1": 1,
"array": [
{
"key_2": 1,
"counter" 3
}
]
}
Run the operation once more. The first query doesn't modifies the document. The second query creates a duplicate:
{
"key_1": 1,
"array": [
{
"key_2": 1,
"counter" 3
}, {
"key_2": 1,
"counter" 1
}
]
}

Please try this :
var key_2Value = 2;
var firstFilterQuery = {
key_1: 1,
array: {
$elemMatch: {
"key_2": key_2Value,
"date": 'someDate',
"conter": { $lte: 3 }
}
}
}
var secondFilterQuery = {
key_1: 1,
"array.key_2": {$ne: key_2Value}
}
var defaultDoc = {key_2 : key_2Value, "date": 'someDefaultDate',counter: 1}
Query :
collection.bulkWrite([
{
updateOne:
{
"filter": firstFilterQuery,
"update": { $inc: { "array.$.conter": 1 } }
}
}, {
updateOne:
{
"filter": secondFilterQuery,
"update": { $push: { array: defaultDoc }
}
}
}
])
With the above query, you can achieve what you wanted in one DB call(at any given case only one 'updateOne' should update the DB), Output should look something like :
{
"acknowledged" : true,
"deletedCount" : 0.0,
"insertedCount" : 0.0,
"matchedCount" : 1.0,
"upsertedCount" : 0.0,
"insertedIds" : {},
"upsertedIds" : {}
}

Related

MongoDB Remove object key based on value

In my application, I have a MongoDB document that will be updated with $inc operation to increase/decrease the number in appliesTo object. Here is a sample object
{
name: "test-document",
appliesTo: {
profiles: {
Profile1: 3,
Profile2: 1
},
tags: {
Tag1: 7,
Tag2: 1
}
}
}
After I'm running the following command
await db.items.updateOne({name: "test-document"}, {$inc: {'appliesTo.profiles.Profile2': -1})
my document will be changed to
{
name: "test-document",
appliesTo: {
profiles: {
Profile1: 3,
Profile2: 0
},
tags: {
Tag1: 7,
Tag2: 1
}
}
}
I'm struggling with writing a query that will remove all keys, which values are 0. The only solution I have currently is to iterate over each key and update it using $unset command. But this is not an atomic operation
Is there a smarter way to handle it in one query?
There is no way to do both operations in a single regular update quey, you can try update with aggregation pipeline starting from MongoDB 4.2,
$cond to check is a key field's value greater than 1 then do $add operation otherwise remove it by $$REMOVE operator
await db.items.updateOne(
{ name: "test-document" },
[{
$set: {
"appliesTo.profiles.Profile2": {
$cond: [
{ $gt: ["$appliesTo.profiles.Profile2", 1] },
{ $add: ["$appliesTo.profiles.Profile2", -1] }
"$$REMOVE"
]
}
}
}]
)
Playground

mongoDB projection on array in an object

I have this document structure in the collection:
{"_id":"890138075223711744",
"guildID":"854557773990854707",
"name":"test-lab",
"game": {
"usedWords":["akşam","elma","akım"]
}
}
What is the most efficient way to get its fields except the array (it can be really large), and at the same time, see if an item exists in the array ?
I tried this:
let query = {_id: channelID}
const options = { sort: { name: 1 }, projection: { name: 1, "game.usedWords": { $elemMatch: { word}}}}
mongoClient.db(db).collection("channels").findOne(query, options);
but I got the error: "$elemMatch can not be used on nested fields"
If I've understood correctly you can use this query:
Using positional operator $ you can return only the matched word.
db.collection.find({
"game.usedWords": "akşam"
},
{
"name": 1,
"game.usedWords.$": 1
})
Example here
The output is only name and the matched word (also _id which is returned by default)
[
{
"_id": "890138075223711744",
"game": {
"usedWords": [
"akşam"
]
},
"name": "test-lab"
}
]

How can I update a document's nested array of object item by 1?

I'm trying to update a mongoose document with the help of findOneAndUpdate but I'm unable to do so. The document looks like this in the database:
{
'docId': 1001,
'totalViews': 3,
'docInfo': [
{
id: 1,
views: 2
},
{
id: 2,
views: 1
}
]
}
I'm trying to update totalViews by 1 which will make the total count to be 4. And I also need to update the second object's views property by 1 in imageInfo array. Which will have a views count of 2.
I tried doing this by first fetching the whole document with the help of:
const doc = await Doc.find({ docId: 1001 });
Then found the index of the docInfo array item which needs to be updated. Which is the object with id 2.
const docIndex = doc[0].docInfo.findIndex( item => {
return item.id === 2;
});
Then used findOneAndUpdate to update the items:
await Doc.findOneAndUpdate(
{ docId: 1001, "docInfo.id": 2 },
{
$set: {
[ `docInfo.${2}.views` ]: 1++,
'totalViews': 1++
}
}, { new: true }
);
With this I'm getting this error:
SyntaxError: Invalid left-hand side expression in postfix operation
What am I doing wrong here?
What you are doing is invalid, you can use $inc operator to increment a number, and don't need to find a query as well,
await Doc.findOneAndUpdate(
{ docId: 1001, "docInfo.id": 2 },
{
$inc: {
'docInfo.$.views': 1,
'totalViews': 1
}
},
{ new: true }
);
Playground

MongoDB assymetrical return of data, first item in array returned in full, the rest with certain properties omitted?

I'm new to MongoDB and getting to grips with its syntax and capabilities. To achieve the functionality described in the title I believe I can create a promise that will run 2 simultaneous queries on the document - one to get the full content of one item in the array (or at least the data that is omitted in the other query, to re-add after), searched for by most recent date, the other to return the array minus specific properties. I have the following document:
{
_id : ObjectId('5rtgwr6gsrtbsr6hsfbsr6bdrfyb'),
uuid : 'something',
mainArray : [
{
id : 1,
title: 'A',
date: 05/06/2020,
array: ['lots','off','stuff']
},
{
id : 2,
title: 'B',
date: 28/05/2020,
array: ['even','more','stuff']
},
{
id : 3,
title: 'C',
date: 27/05/2020,
array: ['mountains','of','knowledge']
}
]
}
and I would like to return
{
uuid : 'something',
mainArray : [
{
id : 1,
title: 'A',
date: 05/06/2020,
array: ['lots','off','stuff']
},
{
id : 2,
title: 'B'
},
{
id : 3,
title: 'C'
}
]
}
How valid and performant is the promise approach versus constructing one query that would achieve this? I have no idea how to perform such 'combined-rule'/conditions in MongoDB, if anyone could give an example?
If your subdocument array you want to omit is not very large. I would just remove it at the application side. Doing processing in MongoDB means you choose to use the compute resources of MongoDB instead of your application. Generally your application is easier and cheaper to scale, so implementation at the application layer is preferable.
But in this exact case it's not too complex to implement it in MongoDB:
db.collection.aggregate([
{
$addFields: { // keep the first element somewhere
first: { $arrayElemAt: [ "$mainArray", 0] }
}
},
{
$project: { // remove the subdocument field
"mainArray.array": false
}
},
{
$addFields: { // join the first element with the rest of the transformed array
mainArray: {
$concatArrays: [
[ // first element
"$first"
],
{ // select elements from the transformed array except the first
$slice: ["$mainArray", 1, { $size: "$mainArray" }]
}
]
}
}
},
{
$project: { // remove the temporary first elemnt
"first": false
}
}
])
MongoDB Playground

nodejs+mongodb access node.js object with document's _id field value in query

I am learning Mongodb to use with NodeJS. The below is my code:
let configMap = {
'1': 10,
'2': 12,
'3': 13
}
let myData = await mongo_groups_collection.find({
_id: {
$in: [1,2,3]
},
active: true
}).project({
'_id': 1,
'name': 1,
'members': 1,
'messages': {
$slice: [-(configMap[_id]), (DEFAULT_PAGE_SIZE_FILL * PAGE_SIZE) + (PAGE_SIZE - ((configMap[_id]) % PAGE_SIZE))] //Need to use _id to get configMap value
}
}).toArray();
I am using "configMap" nodeJS variable in Mongo slice function to retrieve certain number of elements from the array. To retrieve I need to lookup using "_id" field value which I am not getting and encounter _id "undefined" error.
That's because (configMap[_id]) gets compiled in node.js code but not on database as a query. So in code when this query gets compiled it's not able to find _id since you've not declared it as the way you did it for configMap variable.
You're expecting _id value to be actual _id from document. So, in case if you use (configMap[$_id]) it doesn't work that way cause you can't club a javascript object with documents field.
You can still do this using aggregate query like below - where we actually add needed v value as a field to document for further usage :
/** Make `configMap` an array of objects */
var configMap = [
{ k: 1, v: 10 },
{ k: 2, v: 12 },
{ k: 3, v: 13 },
];
let myData = await mongo_groups_collection.aggregate([
{
$match: { _id: { $in: [1, 2, 3] }, active: true }
},
/** For each matched doc we'll iterate on input `configMap` array &
* find respective `v` value from matched object where `k == _id` */
{
$addFields: {
valueToBeSliced: {
$let: {
vars: { matchedK: { $arrayElemAt: [ { $filter: { input: configMap, cond: { $eq: ["$$this.k", "$_id"] } } }, 0 ] } },
in: "$$matchedK.v",
}
}
}
},
{
$project: {
messages: { $slice: ["$messages", { $subtract: [0, "$valueToBeSliced"] }, someInputValue ] },
name: 1, members: 1
}
}
]).toArray();
Ref : $let
Note :
In projection you don't need to mention this '_id': 1 as it's included by default.
Since your query is using .find() you might be using $slice-projection-operator which can't be used in aggregation, instead we need to use $slice-aggregation-operator, both does same thing but have syntax variation, you need to refer to docs for that.
Also in someInputValue you need to pass in value of (DEFAULT_PAGE_SIZE_FILL * PAGE_SIZE) + (PAGE_SIZE - ((configMap[_id]) % PAGE_SIZE)) - So try to use aggregation operators $divide to get the value. We're doing { $subtract: [0, "$valueToBeSliced"] } to convert positive valueToBeSliced to negative as we can't just do like -valueToBeSliced which we usually do in javaScript.

Resources