Mongoose - update last element in array (update document with an aggregator) - node.js

My question builds off this: Get only the last element of array mongoose. However, I'm using Mongoose and want to also update a field in the last element of the array, not just retrieve it.
How can I modify the last element/object in the rounds array to change from status: started to status: terminated?
Document to modify
{
"_id" : "8844d3f2d25f45df8105db1ab058d7d6",
"rounds" : [
{ "status" : "offline" },
{ "status" : "paused" },
{ "status" : "started" }
],
"updatedAt" : ISODate("2020-07-05T21:21:58.823Z")
}
Seems like using negative indices would make this easy, allowing the n-1 element to be modified. I've seen many questions mention aggregation, but none modify the underlying data. I want to combine aggregation with updating a documenting.
Is it possible to use $set with a negative index?

As of v4.2 MongoDB does not have a straightforward way to update an array element by index.
However starting from v4.2 you can use updates with aggregation pipeline, which allow you to construct a new array based on the current array and replace the current array.
Instead of modifying the last element, you can construct a new array consisting of the first top n - 1 elements combining with a new element { status: "terminated" }.
You can achieve this by using $slice to get the top n - 1 elements and combine with the new element using $concatArrays
This assumes the elements only contains the field status you you want to update a field of an array elements that contains multiple fields, you'll need to merge the updated field with the current element using $mergeObjects
Model.updateOne({ // or updateMany with your condition
_id: "8844d3f2d25f45df8105db1ab058d7d6"
}, [{
$set: {
rounds: {
$concatArrays: [ // combine arrays
{
$slice: [ // get the top n - 1 elements
"$rounds",
{ $subtract: [{ $size: "$rounds" }, 1] }
]
},,
[{ status: "terminated" }] // a new element to replace the last element
]
}
}
}])

Related

How to add new fields to the last object in an array in MongoDB?

I have an Array of objects in mongoDB as follow
Initially there is only the heartRate field inside the object. Now I want to add new fields to this object along with the existing heartRate field.
Also there can be multiple objects inside the dailyReadings array. Therefore, I need to add new fields only to the last object using nodejs and expressjs
I tried using the $push method but ended up adding new object intead of adding the fields to the last object. Is there a way to achieve this? Thanks in advance!
Why I am doing this (For further understanding):-
I have developed a mobile app to read the heart rate. Initially it will save the heart rate in the database as a new object (As in the image). Then, there are several other data sent through a desktop application which needs to add to the same object (Which is the last object in the dailyReadings array)
There is no straight way to do this, you can try update with aggregation pipeline starting from MongoDB 4.2,
$size to get total elements in dailyReadings array
$subtract to minus 1 from above total elements
$slice to get elements other than the last object element
$slice to get last object element by -1 from dailyReadings
$arrayElemAt to select first object element from array
$mergeObjects to merge existing object fields of the last object and new fields that you want to insert
$concatArrays to concat first slice array and second updated object
db.collection.update(
{}, // put your query condition
[{
$set: {
dailyReadings: {
$concatArrays: [
{
$slice: [
"$dailyReadings",
0,
{ $subtract: [{ $size: "$dailyReadings" }, 1] }
]
},
[
{
$mergeObjects: [
{ $arrayElemAt: [{ $slice: ["$dailyReadings", -1] }, 0] },
{
newField: "1"
}
]
}
]
]
}
}
}]
)
Playground
In order for you to add fields to the last object, the heartRate should be an object with a schema containing the following
Array (for you to add to)
any other necessary data type you'd want the object to have
you must define a complex schema using mongoose, perform the following changes to your file of model
const mongoose = require('mongoose');
const childSchema = mongoose.Schema({
heartRate: {type: Array, required: true}
array: {type: Array, required: false}, //change the required parameter based on your requirement
});
const parentSchema = mongoose.Schema({
dailyReadings: {
type: childSchema,
required: false //change the required parameter based on your requirement
},
});
module.exports = mongoose.model('modelCollection', parentSchema);
So basically you need to define the child schema, and change the type of dailyReadings to that schema and add to the array of different objects.

MongoDB nested array update multiple documents [duplicate]

I am trying to update a value in the nested array but can't get it to work.
My object is like this
{
"_id": {
"$oid": "1"
},
"array1": [
{
"_id": "12",
"array2": [
{
"_id": "123",
"answeredBy": [], // need to push "success"
},
{
"_id": "124",
"answeredBy": [],
}
],
}
]
}
I need to push a value to "answeredBy" array.
In the below example, I tried pushing "success" string to the "answeredBy" array of the "123 _id" object but it does not work.
callback = function(err,value){
if(err){
res.send(err);
}else{
res.send(value);
}
};
conditions = {
"_id": 1,
"array1._id": 12,
"array2._id": 123
};
updates = {
$push: {
"array2.$.answeredBy": "success"
}
};
options = {
upsert: true
};
Model.update(conditions, updates, options, callback);
I found this link, but its answer only says I should use object like structure instead of array's. This cannot be applied in my situation. I really need my object to be nested in arrays
It would be great if you can help me out here. I've been spending hours to figure this out.
Thank you in advance!
General Scope and Explanation
There are a few things wrong with what you are doing here. Firstly your query conditions. You are referring to several _id values where you should not need to, and at least one of which is not on the top level.
In order to get into a "nested" value and also presuming that _id value is unique and would not appear in any other document, you query form should be like this:
Model.update(
{ "array1.array2._id": "123" },
{ "$push": { "array1.0.array2.$.answeredBy": "success" } },
function(err,numAffected) {
// something with the result in here
}
);
Now that would actually work, but really it is only a fluke that it does as there are very good reasons why it should not work for you.
The important reading is in the official documentation for the positional $ operator under the subject of "Nested Arrays". What this says is:
The positional $ operator cannot be used for queries which traverse more than one array, such as queries that traverse arrays nested within other arrays, because the replacement for the $ placeholder is a single value
Specifically what that means is the element that will be matched and returned in the positional placeholder is the value of the index from the first matching array. This means in your case the matching index on the "top" level array.
So if you look at the query notation as shown, we have "hardcoded" the first ( or 0 index ) position in the top level array, and it just so happens that the matching element within "array2" is also the zero index entry.
To demonstrate this you can change the matching _id value to "124" and the result will $push an new entry onto the element with _id "123" as they are both in the zero index entry of "array1" and that is the value returned to the placeholder.
So that is the general problem with nesting arrays. You could remove one of the levels and you would still be able to $push to the correct element in your "top" array, but there would still be multiple levels.
Try to avoid nesting arrays as you will run into update problems as is shown.
The general case is to "flatten" the things you "think" are "levels" and actually make theses "attributes" on the final detail items. For example, the "flattened" form of the structure in the question should be something like:
{
"answers": [
{ "by": "success", "type2": "123", "type1": "12" }
]
}
Or even when accepting the inner array is $push only, and never updated:
{
"array": [
{ "type1": "12", "type2": "123", "answeredBy": ["success"] },
{ "type1": "12", "type2": "124", "answeredBy": [] }
]
}
Which both lend themselves to atomic updates within the scope of the positional $ operator
MongoDB 3.6 and Above
From MongoDB 3.6 there are new features available to work with nested arrays. This uses the positional filtered $[<identifier>] syntax in order to match the specific elements and apply different conditions through arrayFilters in the update statement:
Model.update(
{
"_id": 1,
"array1": {
"$elemMatch": {
"_id": "12","array2._id": "123"
}
}
},
{
"$push": { "array1.$[outer].array2.$[inner].answeredBy": "success" }
},
{
"arrayFilters": [{ "outer._id": "12" },{ "inner._id": "123" }]
}
)
The "arrayFilters" as passed to the options for .update() or even
.updateOne(), .updateMany(), .findOneAndUpdate() or .bulkWrite() method specifies the conditions to match on the identifier given in the update statement. Any elements that match the condition given will be updated.
Because the structure is "nested", we actually use "multiple filters" as is specified with an "array" of filter definitions as shown. The marked "identifier" is used in matching against the positional filtered $[<identifier>] syntax actually used in the update block of the statement. In this case inner and outer are the identifiers used for each condition as specified with the nested chain.
This new expansion makes the update of nested array content possible, but it does not really help with the practicality of "querying" such data, so the same caveats apply as explained earlier.
You typically really "mean" to express as "attributes", even if your brain initially thinks "nesting", it's just usually a reaction to how you believe the "previous relational parts" come together. In reality you really need more denormalization.
Also see How to Update Multiple Array Elements in mongodb, since these new update operators actually match and update "multiple array elements" rather than just the first, which has been the previous action of positional updates.
NOTE Somewhat ironically, since this is specified in the "options" argument for .update() and like methods, the syntax is generally compatible with all recent release driver versions.
However this is not true of the mongo shell, since the way the method is implemented there ( "ironically for backward compatibility" ) the arrayFilters argument is not recognized and removed by an internal method that parses the options in order to deliver "backward compatibility" with prior MongoDB server versions and a "legacy" .update() API call syntax.
So if you want to use the command in the mongo shell or other "shell based" products ( notably Robo 3T ) you need a latest version from either the development branch or production release as of 3.6 or greater.
See also positional all $[] which also updates "multiple array elements" but without applying to specified conditions and applies to all elements in the array where that is the desired action.
I know this is a very old question, but I just struggled with this problem myself, and found, what I believe to be, a better answer.
A way to solve this problem is to use Sub-Documents. This is done by nesting schemas within your schemas
MainSchema = new mongoose.Schema({
array1: [Array1Schema]
})
Array1Schema = new mongoose.Schema({
array2: [Array2Schema]
})
Array2Schema = new mongoose.Schema({
answeredBy": [...]
})
This way the object will look like the one you show, but now each array are filled with sub-documents. This makes it possible to dot your way into the sub-document you want. Instead of using a .update you then use a .find or .findOne to get the document you want to update.
Main.findOne((
{
_id: 1
}
)
.exec(
function(err, result){
result.array1.id(12).array2.id(123).answeredBy.push('success')
result.save(function(err){
console.log(result)
});
}
)
Haven't used the .push() function this way myself, so the syntax might not be right, but I have used both .set() and .remove(), and both works perfectly fine.

Updating Mongo, but not overwrite current data, just append to it

I need to add to a list of items in Mongo so if I have
items:{item: "apple"}
what would I use to add another item in an object instead of changing that initial object? So I can end up with.
items: {item: "apple"},{item:"orange"},{item:"blueberry"}
Can I use findOneAndUpdate? Or will this over-write the original data. I am having a hard time finding the distinction in the documents.
In closing, what method is used for updating and overwriting and what is used for appending to objects and arrays?
You can use the $addToSet operator.
For example:
db.yourCollection.update(
{ _id: 1 },
{ $addToSet: { items: {item : "orange" } } }
)
The code abode will add the item : "orange" to the items list of the document with id=1

MongoDB save entries the other way round

I use NodeJS and I have a MongoDB collection with a lot of entries. 99% of time the last entry is selected, sometimes the entry before. Since MongoDB has to get through all entries one by one, it would be more useful to sort the entries the other way round:
Instead of this:
{
_id: "foo",
name: "name"
},
{
_id: "bar",
name: "name"
}
// <- new entry will be inserted here
I want to use this:
// <- new entry will be inserted here
{
_id: "foo",
name: "name"
},
{
_id: "bar",
name: "name"
},
So that in most cases the entry I search for is the first or the second item.
Is that possible or even necessary (does it make any difference in speed)?
I could also reverse the items and then iterate through them, but I don't think that this would be faster.
You schould not be concerned about position of the item in the collection. Each collection has an index on _id field so if you sort your collection by this field and take first (second or third) element you will get in in no time ( porbably 0 ms)

Mongoose/Mongodb previous and next in embedded document

I'm learning Mongodb/Mongoose/Express and have come across a fairly complex query (relative to my current level of understanding anyway) that I'm not sure how best to approach. I have a collection - to keep it simple let's call it entities - with an embedded actions array:
name: String
actions: [{
name: String
date: Date
}]
What I'd like to do is to return an array of documents with each containing the most recent action (or most recent to a specified date), and the next action (based on the same date).
Would this be possible with one find() query, or would I need to break this down into multiple queries and merge the results to generate one result array? I'm looking for the most efficient route possible.
Provided that your "actions" are inserted with the "most recent" being the last entry in the list, and usually this will be the case unless you are specifically updating items and changing dates, then all you really want to do is "project" the last item of the array. This is what the $slice projection operation is for:
Model.find({},{ "actions": { "$slice": -1 } },function(err,docs) {
// contains an array with the last item
});
If indeed you are "updating" array items and changing dates, but you want to query for the most recent on a regular basis, then you are probably best off keeping the array ordered. You can do this with a few modifiers such as:
Model.update(
{
"_id": ObjectId("541f7bbb699e6dd5a7caf2d6"),
},
{
"$push": { "actions": { "$each": [], "$sort": { "date": 1 } } }
},
function(err,numAffected) {
}
);
Which is actually more of a trick that you can do with the $sort modifier to simply sort the existing array elements without adding or removing. In versions prior to 2.6 you need the $slice "update" modifier in here as well, but this could be set to a value larger than the expected array elements if you did not actually want to restrict the possible size, but that is probably a good idea.
Unfortunately, if you were "updating" via a $set statement, then you cannot do this "sorting" in a single update statement, as MongoDB will not allow both types of operations on the array at once. But if you can live with that, then this is a way to keep the array ordered so the first query form works.
If it just seems too hard to keep an array ordered by date, then you can in fact retrieve the largest value my means of the .aggregate() method. This allows greater manipulation of the documents than is available to basic queries, at a little more cost:
Model.aggregate([
// Unwind the array to de-normalize as documents
{ "$unwind": "$actions" },
// Sort the contents per document _id and inner date
{ "$sort": { "_id": 1, "actions.date": 1 } },
// Group back with the "last" element only
{ "$group": {
"_id": "$_id",
"name": { "$last": "$name" },
"actions": { "$last": "$actions" }
}}
],
function(err,docs) {
})
And that will "pull apart" the array using the $unwind operator, then process with a next stage to $sort the contents by "date". In the $group pipeline stage the "_id" means to use the original document key to "collect" on, and the $last operator picks the field values from the "last" document ( de-normalized ) on that grouping boundary.
So there are various things that you can do, but of course the best way is to keep your array ordered and use the basic projection operators to simply get the last item in the list.

Resources