Update last item in array
how can I target a last item in array on an instance of that schema:
let TrackerSchema = new Schema({
status: String,
start_date: { type: Date, default: Date.now },
end_date: { type: Date },
companyId: { type: mongoose.Schema.Types.ObjectId, ref: "Company" },
userId: { type: mongoose.Schema.Types.ObjectId, ref: "User" },
pauses: Array,
total_today: Number
});
instance of a tracker looks like that:
{
"pauses": [{
"reason": "Lanch",
"start": "2018-11-21T18:13:22.835Z",
"end": "2018-11-21T18:14:30.835Z"
}, {
"reason": "Lanch",
"start": "2018-11-21T18:15:09.057Z"
}],
"_id": "...",
"status": "pause",
"start_date": "2018-11-21T18:12:43.896Z",
"companyId": "...",
"userId": "...",
"__v": 2
}
I need to get last item in pauses and add an "end" property to it, I tried like that and it doesn't save it, and seems that the right way is to use something like update or findOneAndUpdate?
Tracker.findOne(query, (error, tracker) => {
checkForError(res, error);
let date = new Date();
let lastItem = tracker.pauses.length;
tracker.pauses[lastItem - 1].end = date;
tracker.status = "go";
tracker.save(error => {
checkForError(res, error);
res.send(tracker);
});
});
javascript node.js mongodb mongoose
add a comment |
how can I target a last item in array on an instance of that schema:
let TrackerSchema = new Schema({
status: String,
start_date: { type: Date, default: Date.now },
end_date: { type: Date },
companyId: { type: mongoose.Schema.Types.ObjectId, ref: "Company" },
userId: { type: mongoose.Schema.Types.ObjectId, ref: "User" },
pauses: Array,
total_today: Number
});
instance of a tracker looks like that:
{
"pauses": [{
"reason": "Lanch",
"start": "2018-11-21T18:13:22.835Z",
"end": "2018-11-21T18:14:30.835Z"
}, {
"reason": "Lanch",
"start": "2018-11-21T18:15:09.057Z"
}],
"_id": "...",
"status": "pause",
"start_date": "2018-11-21T18:12:43.896Z",
"companyId": "...",
"userId": "...",
"__v": 2
}
I need to get last item in pauses and add an "end" property to it, I tried like that and it doesn't save it, and seems that the right way is to use something like update or findOneAndUpdate?
Tracker.findOne(query, (error, tracker) => {
checkForError(res, error);
let date = new Date();
let lastItem = tracker.pauses.length;
tracker.pauses[lastItem - 1].end = date;
tracker.status = "go";
tracker.save(error => {
checkForError(res, error);
res.send(tracker);
});
});
javascript node.js mongodb mongoose
add a comment |
how can I target a last item in array on an instance of that schema:
let TrackerSchema = new Schema({
status: String,
start_date: { type: Date, default: Date.now },
end_date: { type: Date },
companyId: { type: mongoose.Schema.Types.ObjectId, ref: "Company" },
userId: { type: mongoose.Schema.Types.ObjectId, ref: "User" },
pauses: Array,
total_today: Number
});
instance of a tracker looks like that:
{
"pauses": [{
"reason": "Lanch",
"start": "2018-11-21T18:13:22.835Z",
"end": "2018-11-21T18:14:30.835Z"
}, {
"reason": "Lanch",
"start": "2018-11-21T18:15:09.057Z"
}],
"_id": "...",
"status": "pause",
"start_date": "2018-11-21T18:12:43.896Z",
"companyId": "...",
"userId": "...",
"__v": 2
}
I need to get last item in pauses and add an "end" property to it, I tried like that and it doesn't save it, and seems that the right way is to use something like update or findOneAndUpdate?
Tracker.findOne(query, (error, tracker) => {
checkForError(res, error);
let date = new Date();
let lastItem = tracker.pauses.length;
tracker.pauses[lastItem - 1].end = date;
tracker.status = "go";
tracker.save(error => {
checkForError(res, error);
res.send(tracker);
});
});
javascript node.js mongodb mongoose
how can I target a last item in array on an instance of that schema:
let TrackerSchema = new Schema({
status: String,
start_date: { type: Date, default: Date.now },
end_date: { type: Date },
companyId: { type: mongoose.Schema.Types.ObjectId, ref: "Company" },
userId: { type: mongoose.Schema.Types.ObjectId, ref: "User" },
pauses: Array,
total_today: Number
});
instance of a tracker looks like that:
{
"pauses": [{
"reason": "Lanch",
"start": "2018-11-21T18:13:22.835Z",
"end": "2018-11-21T18:14:30.835Z"
}, {
"reason": "Lanch",
"start": "2018-11-21T18:15:09.057Z"
}],
"_id": "...",
"status": "pause",
"start_date": "2018-11-21T18:12:43.896Z",
"companyId": "...",
"userId": "...",
"__v": 2
}
I need to get last item in pauses and add an "end" property to it, I tried like that and it doesn't save it, and seems that the right way is to use something like update or findOneAndUpdate?
Tracker.findOne(query, (error, tracker) => {
checkForError(res, error);
let date = new Date();
let lastItem = tracker.pauses.length;
tracker.pauses[lastItem - 1].end = date;
tracker.status = "go";
tracker.save(error => {
checkForError(res, error);
res.send(tracker);
});
});
javascript node.js mongodb mongoose
javascript node.js mongodb mongoose
edited Nov 22 '18 at 8:14
Raza Rafaideen
5731519
5731519
asked Nov 21 '18 at 18:44
FelixFelix
306
306
add a comment |
add a comment |
1 Answer
1
active
oldest
votes
Schema Problems
The basic reason this is not updating for you is because Mongoose actually demands to be told that such changes actually did something. This is essentially related to how it keeps internal references and then decides what to send back to the server as an update()
statement when you actually call save()
.
The "simple" fix for this is to just call the markModified()
method on the path before you save()
:
tracker.markModified('pauses');
tracker.save((error) => {
There is however another way, since the only reason you ever need to call markModified()
is because mongoose has no concept of what you actually changed on the underlying element. Typically people encounter this on the Mixed
type. But in your case you defined Array
on pauses
without specifying he structure of the schema for the items within that array.
Therefore simply adding schema allows the changes without calling markModified()
// pauses: Array // change this line
pauses: [{
reason: String,
start: Date,
end: Date
}]
Once you do either of those the changes to the array element will actually be persisted, but as you note the whole process of findOne()
and then making these changes and calling save()
is not really ideal.
As such, there is a way to to this atomically, but it does require you make some changes.
Atomic Updates
The core problem here is updating the last element of the array. MongoDB does not have any real method for either querying or updating the last element in any efficient manner. This is not a new problem, but the common line is that the solution to this has been around for some time.
Reverse Sort the array
The basic principle is that as you add items to your array you can actually atomically sort all of the elements in the array by a given property so that there is always a certain array order. The reason this is a reverse sort is that what MongoDB is actually very happy with is the first array element.
Therefore in order to keep the array elements ordered so the latest is always the first element, you can apply the $sort
modifier to $push
as follows:
vat newPauses = [{ reason: "Next", start: new Date() }];
Tracker.findOneAndUpdate(
{ _id },
{
'$push': {
'pauses': { '$each': newPauses, '$sort': { start: -1 } }
}
},
{ new: true }
)
That is what your code would look like every time you want to add to the array. Because the new item has a more recent date value in start
the $sort
modifier will re-arrange he array so that this most recent item is actually at the beginning.
You can even update all existing documents in your collection with one simple statement:
Tracker.updateMany(
{},
{
'$push': {
'pauses': { '$each': , '$sort': { start: -1 } }
}
}
)
In this case the $each
is given an empty array, so of course no new items are added to the arrays in any document. But the $sort
is fired and all items will then be re-ordered by their start time.
This method using $sort
suits a lot of people, but there may be cases where it is not the right solution. This is why there is another way.
Prepend to the array
The general case here is where you consider that either you don't really have a property such as the start
which should govern what the order of elements in the array actually is, or even in cases where you don't want to have any possible overhead of applying a $sort
or move existing elements around in any way.
In order to achieve this all that is needed is to add the $position
modifier to your $push
statement:
vat newPauses = [{ reason: "Next", start: new Date() }];
Tracker.findOneAndUpdate(
{ _id },
{
'$push': {
'pauses': { '$each': newPauses, '$position': 0 }
}
},
{ new: true }
)
In short, $position
tells MongoDB where the new element is supposed to go by array index. The 0
of course means the beginning of the array, and this will move all existing members along to the right rather than adding a new member to the right side.
The only drawback here is that you cannot really update the existing array members in a purely reversed order without running a process to loop through all documents and rewrite them in the collection as there is no atomic update that can reverse the whole array in the same way the $sort
can modify the existing array. So the setup cost if changing to using $position
is a little higher.
If you want this type of approach, then the methods for updating your existing collection are extensively documented at Update MongoDB field using value of another field
Demonstration
Of course these are best described by demonstration. Here is a full listing demonstrating all approaches:
const { Schema, Types: { ObjectId } } = mongoose = require('mongoose');
const uri = 'mongodb://localhost:27017/trackdemo';
const opts = { useNewUrlParser: true };
// sensible defaults
mongoose.Promise = global.Promise;
mongoose.set('debug', true);
mongoose.set('useFindAndModify', false);
mongoose.set('useCreateIndex', true);
// schema defs
const pauseSchema = new Schema({
reason: String,
start: Date,
end: Date
})
const trackerSchema = new Schema({
pauses: Array
});
const altTrackerSchema = new Schema({
pauses: [pauseSchema]
});
const Tracker = mongoose.model('Tracker', trackerSchema);
const AltTracker = mongoose.model('AltTracker', altTrackerSchema);
// log helper
const log = data => console.log(JSON.stringify(data, undefined, 2));
// main
const getPauses = () => [
{
reason: "Lunch",
start: new Date("2018-11-21T18:13:22.835Z"),
end: new Date("2018-11-21T18:14:30.835Z")
},
{
reason: "Lunch",
start: new Date("2018-11-21T18:15:09.057Z")
}
];
(async function() {
try {
const conn = await mongoose.connect(uri, opts);
// Clean models
await Promise.all(
Object.entries(conn.models).map(([k,m]) => m.deleteMany())
);
// Test Tracker
await (async function() {
let pauses = getPauses();
let tracker = await Tracker.create({ pauses });
log(tracker);
let temp = tracker.pauses[tracker.pauses.length - 1];
temp.end = new Date();
log(temp);
tracker.pauses[tracker.pauses.length -1] = temp;
tracker.markModified('pauses');
await tracker.save();
let result = await Tracker.findById(tracker._id);
log(result);
})()
// Test AltTracker
await (async function() {
let pauses = getPauses();
let tracker = await AltTracker.create({ pauses });
log(tracker);
let temp = tracker.pauses[tracker.pauses.length - 1];
temp.end = new Date();
log(temp);
tracker.pauses[tracker.pauses.length -1] = temp;
//tracker.markModified('pauses');
await tracker.save();
let result = await AltTracker.findById(tracker._id);
log(result);
})()
// AltTracker atomic $sort method
await (async function() {
let _id = new ObjectId(); // keep ref for tests
let pauses = getPauses();
let tracker = await AltTracker.findOneAndUpdate(
{ _id },
{
'$push': {
'pauses': { '$each': pauses, '$sort': { 'start': -1 } }
}
},
{ 'new': true, 'upsert': true }
);
log(tracker);
// update first
tracker = await AltTracker.findOneAndUpdate(
{ _id, 'pauses.0.end': { '$exists': false } },
{ '$set': { 'pauses.0.end': new Date() } },
{ 'new': true }
);
log(tracker);
})()
// AltTracker atomic $position method
await (async function() {
let _id = new ObjectId(); // keep ref for tests
let pauses = getPauses();
// Doing this twice purely for demo
let tracker = await AltTracker.findOneAndUpdate(
{ _id },
{
'$push': {
'pauses': { '$each': [ pauses[0] ], '$position': 0 }
}
},
{ 'new': true, 'upsert': true }
);
log(tracker);
tracker = await AltTracker.findOneAndUpdate(
{ _id },
{
'$push': {
'pauses': { '$each': [ pauses[1] ], '$position': 0 }
}
},
{ 'new': true, 'upsert': true }
);
log(tracker);
tracker = await AltTracker.findOneAndUpdate(
{ _id, 'pauses.0.end': { '$exists': false } },
{ '$set': { 'pauses.0.end': new Date() } },
{ 'new': true }
);
log(tracker);
})()
} catch(e) {
console.error(e)
} finally {
mongoose.disconnect()
}
})()
And the output demonstrating what happens on all updates:
Mongoose: trackers.deleteMany({}, {})
Mongoose: alttrackers.deleteMany({}, {})
Mongoose: trackers.insertOne({ pauses: [ { reason: 'Lunch', start: new Date("Wed, 21 Nov 2018 18:13:22 GMT"), end: new Date("Wed, 21 Nov 2018 18:14:30 GMT") }, { reason: 'Lunch', start: new Date("Wed, 21 Nov 2018 18:15:09 GMT") } ], _id: ObjectId("5bf65cf6ae7b8639c3f5090d"), __v: 0 })
{
"pauses": [
{
"reason": "Lunch",
"start": "2018-11-21T18:13:22.835Z",
"end": "2018-11-21T18:14:30.835Z"
},
{
"reason": "Lunch",
"start": "2018-11-21T18:15:09.057Z"
}
],
"_id": "5bf65cf6ae7b8639c3f5090d",
"__v": 0
}
{
"reason": "Lunch",
"start": "2018-11-21T18:15:09.057Z",
"end": "2018-11-22T07:38:30.883Z"
}
Mongoose: trackers.updateOne({ _id: ObjectId("5bf65cf6ae7b8639c3f5090d"), __v: 0 }, { '$set': { pauses: [ { reason: 'Lunch', start: new Date("Wed, 21 Nov 2018 18:13:22 GMT"), end: new Date("Wed, 21 Nov 2018 18:14:30 GMT") }, { reason: 'Lunch', start: new Date("Wed, 21 Nov 2018 18:15:09 GMT"), end: new Date("Thu, 22 Nov 2018 07:38:30 GMT") } ] }, '$inc': { __v: 1 } })
Mongoose: trackers.findOne({ _id: ObjectId("5bf65cf6ae7b8639c3f5090d") }, { projection: {} })
{
"pauses": [
{
"reason": "Lunch",
"start": "2018-11-21T18:13:22.835Z",
"end": "2018-11-21T18:14:30.835Z"
},
{
"reason": "Lunch",
"start": "2018-11-21T18:15:09.057Z",
"end": "2018-11-22T07:38:30.883Z"
}
],
"_id": "5bf65cf6ae7b8639c3f5090d",
"__v": 1
}
Mongoose: alttrackers.insertOne({ _id: ObjectId("5bf65cf6ae7b8639c3f5090e"), pauses: [ { _id: ObjectId("5bf65cf6ae7b8639c3f50910"), reason: 'Lunch', start: new Date("Wed, 21 Nov 2018 18:13:22 GMT"), end: new Date("Wed, 21 Nov 2018 18:14:30 GMT") }, { _id: ObjectId("5bf65cf6ae7b8639c3f5090f"), reason: 'Lunch', start: new Date("Wed, 21 Nov 2018 18:15:09 GMT") } ], __v: 0 })
{
"_id": "5bf65cf6ae7b8639c3f5090e",
"pauses": [
{
"_id": "5bf65cf6ae7b8639c3f50910",
"reason": "Lunch",
"start": "2018-11-21T18:13:22.835Z",
"end": "2018-11-21T18:14:30.835Z"
},
{
"_id": "5bf65cf6ae7b8639c3f5090f",
"reason": "Lunch",
"start": "2018-11-21T18:15:09.057Z"
}
],
"__v": 0
}
{
"_id": "5bf65cf6ae7b8639c3f5090f",
"reason": "Lunch",
"start": "2018-11-21T18:15:09.057Z",
"end": "2018-11-22T07:38:30.915Z"
}
Mongoose: alttrackers.updateOne({ _id: ObjectId("5bf65cf6ae7b8639c3f5090e"), __v: 0 }, { '$set': { 'pauses.1.end': new Date("Thu, 22 Nov 2018 07:38:30 GMT") } })
Mongoose: alttrackers.findOne({ _id: ObjectId("5bf65cf6ae7b8639c3f5090e") }, { projection: {} })
{
"_id": "5bf65cf6ae7b8639c3f5090e",
"pauses": [
{
"_id": "5bf65cf6ae7b8639c3f50910",
"reason": "Lunch",
"start": "2018-11-21T18:13:22.835Z",
"end": "2018-11-21T18:14:30.835Z"
},
{
"_id": "5bf65cf6ae7b8639c3f5090f",
"reason": "Lunch",
"start": "2018-11-21T18:15:09.057Z",
"end": "2018-11-22T07:38:30.915Z"
}
],
"__v": 0
}
Mongoose: alttrackers.findOneAndUpdate({ _id: ObjectId("5bf65cf6ae7b8639c3f50911") }, { '$setOnInsert': { __v: 0 }, '$push': { pauses: { '$each': [ { _id: ObjectId("5bf65cf6ae7b8639c3f50913"), reason: 'Lunch', start: new Date("Wed, 21 Nov 2018 18:13:22 GMT"), end: new Date("Wed, 21 Nov 2018 18:14:30 GMT") }, { _id: ObjectId("5bf65cf6ae7b8639c3f50912"), reason: 'Lunch', start: new Date("Wed, 21 Nov 2018 18:15:09 GMT") } ], '$sort': { start: -1 } } } }, { upsert: true, remove: false, projection: {}, returnOriginal: false })
{
"_id": "5bf65cf6ae7b8639c3f50911",
"__v": 0,
"pauses": [
{
"_id": "5bf65cf6ae7b8639c3f50912",
"reason": "Lunch",
"start": "2018-11-21T18:15:09.057Z"
},
{
"_id": "5bf65cf6ae7b8639c3f50913",
"reason": "Lunch",
"start": "2018-11-21T18:13:22.835Z",
"end": "2018-11-21T18:14:30.835Z"
}
]
}
Mongoose: alttrackers.findOneAndUpdate({ _id: ObjectId("5bf65cf6ae7b8639c3f50911"), 'pauses.0.end': { '$exists': false } }, { '$set': { 'pauses.0.end': new Date("Thu, 22 Nov 2018 07:38:30 GMT") } }, { upsert: false, remove: false, projection: {}, returnOriginal: false })
{
"_id": "5bf65cf6ae7b8639c3f50911",
"__v": 0,
"pauses": [
{
"_id": "5bf65cf6ae7b8639c3f50912",
"reason": "Lunch",
"start": "2018-11-21T18:15:09.057Z",
"end": "2018-11-22T07:38:30.940Z"
},
{
"_id": "5bf65cf6ae7b8639c3f50913",
"reason": "Lunch",
"start": "2018-11-21T18:13:22.835Z",
"end": "2018-11-21T18:14:30.835Z"
}
]
}
Mongoose: alttrackers.findOneAndUpdate({ _id: ObjectId("5bf65cf6ae7b8639c3f50914") }, { '$setOnInsert': { __v: 0 }, '$push': { pauses: { '$each': [ { _id: ObjectId("5bf65cf6ae7b8639c3f50915"), reason: 'Lunch', start: new Date("Wed, 21 Nov 2018 18:13:22 GMT"), end: new Date("Wed, 21 Nov 2018 18:14:30 GMT") } ], '$position': 0 } } }, { upsert: true, remove: false, projection: {}, returnOriginal: false })
{
"_id": "5bf65cf6ae7b8639c3f50914",
"__v": 0,
"pauses": [
{
"_id": "5bf65cf6ae7b8639c3f50915",
"reason": "Lunch",
"start": "2018-11-21T18:13:22.835Z",
"end": "2018-11-21T18:14:30.835Z"
}
]
}
Mongoose: alttrackers.findOneAndUpdate({ _id: ObjectId("5bf65cf6ae7b8639c3f50914") }, { '$setOnInsert': { __v: 0 }, '$push': { pauses: { '$each': [ { _id: ObjectId("5bf65cf6ae7b8639c3f50916"), reason: 'Lunch', start: new Date("Wed, 21 Nov 2018 18:15:09 GMT") } ], '$position': 0 } } }, { upsert: true, remove: false, projection: {}, returnOriginal: false })
{
"_id": "5bf65cf6ae7b8639c3f50914",
"__v": 0,
"pauses": [
{
"_id": "5bf65cf6ae7b8639c3f50916",
"reason": "Lunch",
"start": "2018-11-21T18:15:09.057Z"
},
{
"_id": "5bf65cf6ae7b8639c3f50915",
"reason": "Lunch",
"start": "2018-11-21T18:13:22.835Z",
"end": "2018-11-21T18:14:30.835Z"
}
]
}
Mongoose: alttrackers.findOneAndUpdate({ _id: ObjectId("5bf65cf6ae7b8639c3f50914"), 'pauses.0.end': { '$exists': false } }, { '$set': { 'pauses.0.end': new Date("Thu, 22 Nov 2018 07:38:30 GMT") } }, { upsert: false, remove: false, projection: {}, returnOriginal: false })
{
"_id": "5bf65cf6ae7b8639c3f50914",
"__v": 0,
"pauses": [
{
"_id": "5bf65cf6ae7b8639c3f50916",
"reason": "Lunch",
"start": "2018-11-21T18:15:09.057Z",
"end": "2018-11-22T07:38:30.957Z"
},
{
"_id": "5bf65cf6ae7b8639c3f50915",
"reason": "Lunch",
"start": "2018-11-21T18:13:22.835Z",
"end": "2018-11-21T18:14:30.835Z"
}
]
}
1
Thank you very much. Your answer not only helps me to solve the problem but also gives me understanding of the matter.
– Felix
Nov 22 '18 at 19:33
add a comment |
Your Answer
StackExchange.ifUsing("editor", function () {
StackExchange.using("externalEditor", function () {
StackExchange.using("snippets", function () {
StackExchange.snippets.init();
});
});
}, "code-snippets");
StackExchange.ready(function() {
var channelOptions = {
tags: "".split(" "),
id: "1"
};
initTagRenderer("".split(" "), "".split(" "), channelOptions);
StackExchange.using("externalEditor", function() {
// Have to fire editor after snippets, if snippets enabled
if (StackExchange.settings.snippets.snippetsEnabled) {
StackExchange.using("snippets", function() {
createEditor();
});
}
else {
createEditor();
}
});
function createEditor() {
StackExchange.prepareEditor({
heartbeatType: 'answer',
autoActivateHeartbeat: false,
convertImagesToLinks: true,
noModals: true,
showLowRepImageUploadWarning: true,
reputationToPostImages: 10,
bindNavPrevention: true,
postfix: "",
imageUploader: {
brandingHtml: "Powered by u003ca class="icon-imgur-white" href="https://imgur.com/"u003eu003c/au003e",
contentPolicyHtml: "User contributions licensed under u003ca href="https://creativecommons.org/licenses/by-sa/3.0/"u003ecc by-sa 3.0 with attribution requiredu003c/au003e u003ca href="https://stackoverflow.com/legal/content-policy"u003e(content policy)u003c/au003e",
allowUrls: true
},
onDemand: true,
discardSelector: ".discard-answer"
,immediatelyShowMarkdownHelp:true
});
}
});
Sign up or log in
StackExchange.ready(function () {
StackExchange.helpers.onClickDraftSave('#login-link');
});
Sign up using Google
Sign up using Facebook
Sign up using Email and Password
Post as a guest
Required, but never shown
StackExchange.ready(
function () {
StackExchange.openid.initPostLogin('.new-post-login', 'https%3a%2f%2fstackoverflow.com%2fquestions%2f53418669%2fupdate-last-item-in-array%23new-answer', 'question_page');
}
);
Post as a guest
Required, but never shown
1 Answer
1
active
oldest
votes
1 Answer
1
active
oldest
votes
active
oldest
votes
active
oldest
votes
Schema Problems
The basic reason this is not updating for you is because Mongoose actually demands to be told that such changes actually did something. This is essentially related to how it keeps internal references and then decides what to send back to the server as an update()
statement when you actually call save()
.
The "simple" fix for this is to just call the markModified()
method on the path before you save()
:
tracker.markModified('pauses');
tracker.save((error) => {
There is however another way, since the only reason you ever need to call markModified()
is because mongoose has no concept of what you actually changed on the underlying element. Typically people encounter this on the Mixed
type. But in your case you defined Array
on pauses
without specifying he structure of the schema for the items within that array.
Therefore simply adding schema allows the changes without calling markModified()
// pauses: Array // change this line
pauses: [{
reason: String,
start: Date,
end: Date
}]
Once you do either of those the changes to the array element will actually be persisted, but as you note the whole process of findOne()
and then making these changes and calling save()
is not really ideal.
As such, there is a way to to this atomically, but it does require you make some changes.
Atomic Updates
The core problem here is updating the last element of the array. MongoDB does not have any real method for either querying or updating the last element in any efficient manner. This is not a new problem, but the common line is that the solution to this has been around for some time.
Reverse Sort the array
The basic principle is that as you add items to your array you can actually atomically sort all of the elements in the array by a given property so that there is always a certain array order. The reason this is a reverse sort is that what MongoDB is actually very happy with is the first array element.
Therefore in order to keep the array elements ordered so the latest is always the first element, you can apply the $sort
modifier to $push
as follows:
vat newPauses = [{ reason: "Next", start: new Date() }];
Tracker.findOneAndUpdate(
{ _id },
{
'$push': {
'pauses': { '$each': newPauses, '$sort': { start: -1 } }
}
},
{ new: true }
)
That is what your code would look like every time you want to add to the array. Because the new item has a more recent date value in start
the $sort
modifier will re-arrange he array so that this most recent item is actually at the beginning.
You can even update all existing documents in your collection with one simple statement:
Tracker.updateMany(
{},
{
'$push': {
'pauses': { '$each': , '$sort': { start: -1 } }
}
}
)
In this case the $each
is given an empty array, so of course no new items are added to the arrays in any document. But the $sort
is fired and all items will then be re-ordered by their start time.
This method using $sort
suits a lot of people, but there may be cases where it is not the right solution. This is why there is another way.
Prepend to the array
The general case here is where you consider that either you don't really have a property such as the start
which should govern what the order of elements in the array actually is, or even in cases where you don't want to have any possible overhead of applying a $sort
or move existing elements around in any way.
In order to achieve this all that is needed is to add the $position
modifier to your $push
statement:
vat newPauses = [{ reason: "Next", start: new Date() }];
Tracker.findOneAndUpdate(
{ _id },
{
'$push': {
'pauses': { '$each': newPauses, '$position': 0 }
}
},
{ new: true }
)
In short, $position
tells MongoDB where the new element is supposed to go by array index. The 0
of course means the beginning of the array, and this will move all existing members along to the right rather than adding a new member to the right side.
The only drawback here is that you cannot really update the existing array members in a purely reversed order without running a process to loop through all documents and rewrite them in the collection as there is no atomic update that can reverse the whole array in the same way the $sort
can modify the existing array. So the setup cost if changing to using $position
is a little higher.
If you want this type of approach, then the methods for updating your existing collection are extensively documented at Update MongoDB field using value of another field
Demonstration
Of course these are best described by demonstration. Here is a full listing demonstrating all approaches:
const { Schema, Types: { ObjectId } } = mongoose = require('mongoose');
const uri = 'mongodb://localhost:27017/trackdemo';
const opts = { useNewUrlParser: true };
// sensible defaults
mongoose.Promise = global.Promise;
mongoose.set('debug', true);
mongoose.set('useFindAndModify', false);
mongoose.set('useCreateIndex', true);
// schema defs
const pauseSchema = new Schema({
reason: String,
start: Date,
end: Date
})
const trackerSchema = new Schema({
pauses: Array
});
const altTrackerSchema = new Schema({
pauses: [pauseSchema]
});
const Tracker = mongoose.model('Tracker', trackerSchema);
const AltTracker = mongoose.model('AltTracker', altTrackerSchema);
// log helper
const log = data => console.log(JSON.stringify(data, undefined, 2));
// main
const getPauses = () => [
{
reason: "Lunch",
start: new Date("2018-11-21T18:13:22.835Z"),
end: new Date("2018-11-21T18:14:30.835Z")
},
{
reason: "Lunch",
start: new Date("2018-11-21T18:15:09.057Z")
}
];
(async function() {
try {
const conn = await mongoose.connect(uri, opts);
// Clean models
await Promise.all(
Object.entries(conn.models).map(([k,m]) => m.deleteMany())
);
// Test Tracker
await (async function() {
let pauses = getPauses();
let tracker = await Tracker.create({ pauses });
log(tracker);
let temp = tracker.pauses[tracker.pauses.length - 1];
temp.end = new Date();
log(temp);
tracker.pauses[tracker.pauses.length -1] = temp;
tracker.markModified('pauses');
await tracker.save();
let result = await Tracker.findById(tracker._id);
log(result);
})()
// Test AltTracker
await (async function() {
let pauses = getPauses();
let tracker = await AltTracker.create({ pauses });
log(tracker);
let temp = tracker.pauses[tracker.pauses.length - 1];
temp.end = new Date();
log(temp);
tracker.pauses[tracker.pauses.length -1] = temp;
//tracker.markModified('pauses');
await tracker.save();
let result = await AltTracker.findById(tracker._id);
log(result);
})()
// AltTracker atomic $sort method
await (async function() {
let _id = new ObjectId(); // keep ref for tests
let pauses = getPauses();
let tracker = await AltTracker.findOneAndUpdate(
{ _id },
{
'$push': {
'pauses': { '$each': pauses, '$sort': { 'start': -1 } }
}
},
{ 'new': true, 'upsert': true }
);
log(tracker);
// update first
tracker = await AltTracker.findOneAndUpdate(
{ _id, 'pauses.0.end': { '$exists': false } },
{ '$set': { 'pauses.0.end': new Date() } },
{ 'new': true }
);
log(tracker);
})()
// AltTracker atomic $position method
await (async function() {
let _id = new ObjectId(); // keep ref for tests
let pauses = getPauses();
// Doing this twice purely for demo
let tracker = await AltTracker.findOneAndUpdate(
{ _id },
{
'$push': {
'pauses': { '$each': [ pauses[0] ], '$position': 0 }
}
},
{ 'new': true, 'upsert': true }
);
log(tracker);
tracker = await AltTracker.findOneAndUpdate(
{ _id },
{
'$push': {
'pauses': { '$each': [ pauses[1] ], '$position': 0 }
}
},
{ 'new': true, 'upsert': true }
);
log(tracker);
tracker = await AltTracker.findOneAndUpdate(
{ _id, 'pauses.0.end': { '$exists': false } },
{ '$set': { 'pauses.0.end': new Date() } },
{ 'new': true }
);
log(tracker);
})()
} catch(e) {
console.error(e)
} finally {
mongoose.disconnect()
}
})()
And the output demonstrating what happens on all updates:
Mongoose: trackers.deleteMany({}, {})
Mongoose: alttrackers.deleteMany({}, {})
Mongoose: trackers.insertOne({ pauses: [ { reason: 'Lunch', start: new Date("Wed, 21 Nov 2018 18:13:22 GMT"), end: new Date("Wed, 21 Nov 2018 18:14:30 GMT") }, { reason: 'Lunch', start: new Date("Wed, 21 Nov 2018 18:15:09 GMT") } ], _id: ObjectId("5bf65cf6ae7b8639c3f5090d"), __v: 0 })
{
"pauses": [
{
"reason": "Lunch",
"start": "2018-11-21T18:13:22.835Z",
"end": "2018-11-21T18:14:30.835Z"
},
{
"reason": "Lunch",
"start": "2018-11-21T18:15:09.057Z"
}
],
"_id": "5bf65cf6ae7b8639c3f5090d",
"__v": 0
}
{
"reason": "Lunch",
"start": "2018-11-21T18:15:09.057Z",
"end": "2018-11-22T07:38:30.883Z"
}
Mongoose: trackers.updateOne({ _id: ObjectId("5bf65cf6ae7b8639c3f5090d"), __v: 0 }, { '$set': { pauses: [ { reason: 'Lunch', start: new Date("Wed, 21 Nov 2018 18:13:22 GMT"), end: new Date("Wed, 21 Nov 2018 18:14:30 GMT") }, { reason: 'Lunch', start: new Date("Wed, 21 Nov 2018 18:15:09 GMT"), end: new Date("Thu, 22 Nov 2018 07:38:30 GMT") } ] }, '$inc': { __v: 1 } })
Mongoose: trackers.findOne({ _id: ObjectId("5bf65cf6ae7b8639c3f5090d") }, { projection: {} })
{
"pauses": [
{
"reason": "Lunch",
"start": "2018-11-21T18:13:22.835Z",
"end": "2018-11-21T18:14:30.835Z"
},
{
"reason": "Lunch",
"start": "2018-11-21T18:15:09.057Z",
"end": "2018-11-22T07:38:30.883Z"
}
],
"_id": "5bf65cf6ae7b8639c3f5090d",
"__v": 1
}
Mongoose: alttrackers.insertOne({ _id: ObjectId("5bf65cf6ae7b8639c3f5090e"), pauses: [ { _id: ObjectId("5bf65cf6ae7b8639c3f50910"), reason: 'Lunch', start: new Date("Wed, 21 Nov 2018 18:13:22 GMT"), end: new Date("Wed, 21 Nov 2018 18:14:30 GMT") }, { _id: ObjectId("5bf65cf6ae7b8639c3f5090f"), reason: 'Lunch', start: new Date("Wed, 21 Nov 2018 18:15:09 GMT") } ], __v: 0 })
{
"_id": "5bf65cf6ae7b8639c3f5090e",
"pauses": [
{
"_id": "5bf65cf6ae7b8639c3f50910",
"reason": "Lunch",
"start": "2018-11-21T18:13:22.835Z",
"end": "2018-11-21T18:14:30.835Z"
},
{
"_id": "5bf65cf6ae7b8639c3f5090f",
"reason": "Lunch",
"start": "2018-11-21T18:15:09.057Z"
}
],
"__v": 0
}
{
"_id": "5bf65cf6ae7b8639c3f5090f",
"reason": "Lunch",
"start": "2018-11-21T18:15:09.057Z",
"end": "2018-11-22T07:38:30.915Z"
}
Mongoose: alttrackers.updateOne({ _id: ObjectId("5bf65cf6ae7b8639c3f5090e"), __v: 0 }, { '$set': { 'pauses.1.end': new Date("Thu, 22 Nov 2018 07:38:30 GMT") } })
Mongoose: alttrackers.findOne({ _id: ObjectId("5bf65cf6ae7b8639c3f5090e") }, { projection: {} })
{
"_id": "5bf65cf6ae7b8639c3f5090e",
"pauses": [
{
"_id": "5bf65cf6ae7b8639c3f50910",
"reason": "Lunch",
"start": "2018-11-21T18:13:22.835Z",
"end": "2018-11-21T18:14:30.835Z"
},
{
"_id": "5bf65cf6ae7b8639c3f5090f",
"reason": "Lunch",
"start": "2018-11-21T18:15:09.057Z",
"end": "2018-11-22T07:38:30.915Z"
}
],
"__v": 0
}
Mongoose: alttrackers.findOneAndUpdate({ _id: ObjectId("5bf65cf6ae7b8639c3f50911") }, { '$setOnInsert': { __v: 0 }, '$push': { pauses: { '$each': [ { _id: ObjectId("5bf65cf6ae7b8639c3f50913"), reason: 'Lunch', start: new Date("Wed, 21 Nov 2018 18:13:22 GMT"), end: new Date("Wed, 21 Nov 2018 18:14:30 GMT") }, { _id: ObjectId("5bf65cf6ae7b8639c3f50912"), reason: 'Lunch', start: new Date("Wed, 21 Nov 2018 18:15:09 GMT") } ], '$sort': { start: -1 } } } }, { upsert: true, remove: false, projection: {}, returnOriginal: false })
{
"_id": "5bf65cf6ae7b8639c3f50911",
"__v": 0,
"pauses": [
{
"_id": "5bf65cf6ae7b8639c3f50912",
"reason": "Lunch",
"start": "2018-11-21T18:15:09.057Z"
},
{
"_id": "5bf65cf6ae7b8639c3f50913",
"reason": "Lunch",
"start": "2018-11-21T18:13:22.835Z",
"end": "2018-11-21T18:14:30.835Z"
}
]
}
Mongoose: alttrackers.findOneAndUpdate({ _id: ObjectId("5bf65cf6ae7b8639c3f50911"), 'pauses.0.end': { '$exists': false } }, { '$set': { 'pauses.0.end': new Date("Thu, 22 Nov 2018 07:38:30 GMT") } }, { upsert: false, remove: false, projection: {}, returnOriginal: false })
{
"_id": "5bf65cf6ae7b8639c3f50911",
"__v": 0,
"pauses": [
{
"_id": "5bf65cf6ae7b8639c3f50912",
"reason": "Lunch",
"start": "2018-11-21T18:15:09.057Z",
"end": "2018-11-22T07:38:30.940Z"
},
{
"_id": "5bf65cf6ae7b8639c3f50913",
"reason": "Lunch",
"start": "2018-11-21T18:13:22.835Z",
"end": "2018-11-21T18:14:30.835Z"
}
]
}
Mongoose: alttrackers.findOneAndUpdate({ _id: ObjectId("5bf65cf6ae7b8639c3f50914") }, { '$setOnInsert': { __v: 0 }, '$push': { pauses: { '$each': [ { _id: ObjectId("5bf65cf6ae7b8639c3f50915"), reason: 'Lunch', start: new Date("Wed, 21 Nov 2018 18:13:22 GMT"), end: new Date("Wed, 21 Nov 2018 18:14:30 GMT") } ], '$position': 0 } } }, { upsert: true, remove: false, projection: {}, returnOriginal: false })
{
"_id": "5bf65cf6ae7b8639c3f50914",
"__v": 0,
"pauses": [
{
"_id": "5bf65cf6ae7b8639c3f50915",
"reason": "Lunch",
"start": "2018-11-21T18:13:22.835Z",
"end": "2018-11-21T18:14:30.835Z"
}
]
}
Mongoose: alttrackers.findOneAndUpdate({ _id: ObjectId("5bf65cf6ae7b8639c3f50914") }, { '$setOnInsert': { __v: 0 }, '$push': { pauses: { '$each': [ { _id: ObjectId("5bf65cf6ae7b8639c3f50916"), reason: 'Lunch', start: new Date("Wed, 21 Nov 2018 18:15:09 GMT") } ], '$position': 0 } } }, { upsert: true, remove: false, projection: {}, returnOriginal: false })
{
"_id": "5bf65cf6ae7b8639c3f50914",
"__v": 0,
"pauses": [
{
"_id": "5bf65cf6ae7b8639c3f50916",
"reason": "Lunch",
"start": "2018-11-21T18:15:09.057Z"
},
{
"_id": "5bf65cf6ae7b8639c3f50915",
"reason": "Lunch",
"start": "2018-11-21T18:13:22.835Z",
"end": "2018-11-21T18:14:30.835Z"
}
]
}
Mongoose: alttrackers.findOneAndUpdate({ _id: ObjectId("5bf65cf6ae7b8639c3f50914"), 'pauses.0.end': { '$exists': false } }, { '$set': { 'pauses.0.end': new Date("Thu, 22 Nov 2018 07:38:30 GMT") } }, { upsert: false, remove: false, projection: {}, returnOriginal: false })
{
"_id": "5bf65cf6ae7b8639c3f50914",
"__v": 0,
"pauses": [
{
"_id": "5bf65cf6ae7b8639c3f50916",
"reason": "Lunch",
"start": "2018-11-21T18:15:09.057Z",
"end": "2018-11-22T07:38:30.957Z"
},
{
"_id": "5bf65cf6ae7b8639c3f50915",
"reason": "Lunch",
"start": "2018-11-21T18:13:22.835Z",
"end": "2018-11-21T18:14:30.835Z"
}
]
}
1
Thank you very much. Your answer not only helps me to solve the problem but also gives me understanding of the matter.
– Felix
Nov 22 '18 at 19:33
add a comment |
Schema Problems
The basic reason this is not updating for you is because Mongoose actually demands to be told that such changes actually did something. This is essentially related to how it keeps internal references and then decides what to send back to the server as an update()
statement when you actually call save()
.
The "simple" fix for this is to just call the markModified()
method on the path before you save()
:
tracker.markModified('pauses');
tracker.save((error) => {
There is however another way, since the only reason you ever need to call markModified()
is because mongoose has no concept of what you actually changed on the underlying element. Typically people encounter this on the Mixed
type. But in your case you defined Array
on pauses
without specifying he structure of the schema for the items within that array.
Therefore simply adding schema allows the changes without calling markModified()
// pauses: Array // change this line
pauses: [{
reason: String,
start: Date,
end: Date
}]
Once you do either of those the changes to the array element will actually be persisted, but as you note the whole process of findOne()
and then making these changes and calling save()
is not really ideal.
As such, there is a way to to this atomically, but it does require you make some changes.
Atomic Updates
The core problem here is updating the last element of the array. MongoDB does not have any real method for either querying or updating the last element in any efficient manner. This is not a new problem, but the common line is that the solution to this has been around for some time.
Reverse Sort the array
The basic principle is that as you add items to your array you can actually atomically sort all of the elements in the array by a given property so that there is always a certain array order. The reason this is a reverse sort is that what MongoDB is actually very happy with is the first array element.
Therefore in order to keep the array elements ordered so the latest is always the first element, you can apply the $sort
modifier to $push
as follows:
vat newPauses = [{ reason: "Next", start: new Date() }];
Tracker.findOneAndUpdate(
{ _id },
{
'$push': {
'pauses': { '$each': newPauses, '$sort': { start: -1 } }
}
},
{ new: true }
)
That is what your code would look like every time you want to add to the array. Because the new item has a more recent date value in start
the $sort
modifier will re-arrange he array so that this most recent item is actually at the beginning.
You can even update all existing documents in your collection with one simple statement:
Tracker.updateMany(
{},
{
'$push': {
'pauses': { '$each': , '$sort': { start: -1 } }
}
}
)
In this case the $each
is given an empty array, so of course no new items are added to the arrays in any document. But the $sort
is fired and all items will then be re-ordered by their start time.
This method using $sort
suits a lot of people, but there may be cases where it is not the right solution. This is why there is another way.
Prepend to the array
The general case here is where you consider that either you don't really have a property such as the start
which should govern what the order of elements in the array actually is, or even in cases where you don't want to have any possible overhead of applying a $sort
or move existing elements around in any way.
In order to achieve this all that is needed is to add the $position
modifier to your $push
statement:
vat newPauses = [{ reason: "Next", start: new Date() }];
Tracker.findOneAndUpdate(
{ _id },
{
'$push': {
'pauses': { '$each': newPauses, '$position': 0 }
}
},
{ new: true }
)
In short, $position
tells MongoDB where the new element is supposed to go by array index. The 0
of course means the beginning of the array, and this will move all existing members along to the right rather than adding a new member to the right side.
The only drawback here is that you cannot really update the existing array members in a purely reversed order without running a process to loop through all documents and rewrite them in the collection as there is no atomic update that can reverse the whole array in the same way the $sort
can modify the existing array. So the setup cost if changing to using $position
is a little higher.
If you want this type of approach, then the methods for updating your existing collection are extensively documented at Update MongoDB field using value of another field
Demonstration
Of course these are best described by demonstration. Here is a full listing demonstrating all approaches:
const { Schema, Types: { ObjectId } } = mongoose = require('mongoose');
const uri = 'mongodb://localhost:27017/trackdemo';
const opts = { useNewUrlParser: true };
// sensible defaults
mongoose.Promise = global.Promise;
mongoose.set('debug', true);
mongoose.set('useFindAndModify', false);
mongoose.set('useCreateIndex', true);
// schema defs
const pauseSchema = new Schema({
reason: String,
start: Date,
end: Date
})
const trackerSchema = new Schema({
pauses: Array
});
const altTrackerSchema = new Schema({
pauses: [pauseSchema]
});
const Tracker = mongoose.model('Tracker', trackerSchema);
const AltTracker = mongoose.model('AltTracker', altTrackerSchema);
// log helper
const log = data => console.log(JSON.stringify(data, undefined, 2));
// main
const getPauses = () => [
{
reason: "Lunch",
start: new Date("2018-11-21T18:13:22.835Z"),
end: new Date("2018-11-21T18:14:30.835Z")
},
{
reason: "Lunch",
start: new Date("2018-11-21T18:15:09.057Z")
}
];
(async function() {
try {
const conn = await mongoose.connect(uri, opts);
// Clean models
await Promise.all(
Object.entries(conn.models).map(([k,m]) => m.deleteMany())
);
// Test Tracker
await (async function() {
let pauses = getPauses();
let tracker = await Tracker.create({ pauses });
log(tracker);
let temp = tracker.pauses[tracker.pauses.length - 1];
temp.end = new Date();
log(temp);
tracker.pauses[tracker.pauses.length -1] = temp;
tracker.markModified('pauses');
await tracker.save();
let result = await Tracker.findById(tracker._id);
log(result);
})()
// Test AltTracker
await (async function() {
let pauses = getPauses();
let tracker = await AltTracker.create({ pauses });
log(tracker);
let temp = tracker.pauses[tracker.pauses.length - 1];
temp.end = new Date();
log(temp);
tracker.pauses[tracker.pauses.length -1] = temp;
//tracker.markModified('pauses');
await tracker.save();
let result = await AltTracker.findById(tracker._id);
log(result);
})()
// AltTracker atomic $sort method
await (async function() {
let _id = new ObjectId(); // keep ref for tests
let pauses = getPauses();
let tracker = await AltTracker.findOneAndUpdate(
{ _id },
{
'$push': {
'pauses': { '$each': pauses, '$sort': { 'start': -1 } }
}
},
{ 'new': true, 'upsert': true }
);
log(tracker);
// update first
tracker = await AltTracker.findOneAndUpdate(
{ _id, 'pauses.0.end': { '$exists': false } },
{ '$set': { 'pauses.0.end': new Date() } },
{ 'new': true }
);
log(tracker);
})()
// AltTracker atomic $position method
await (async function() {
let _id = new ObjectId(); // keep ref for tests
let pauses = getPauses();
// Doing this twice purely for demo
let tracker = await AltTracker.findOneAndUpdate(
{ _id },
{
'$push': {
'pauses': { '$each': [ pauses[0] ], '$position': 0 }
}
},
{ 'new': true, 'upsert': true }
);
log(tracker);
tracker = await AltTracker.findOneAndUpdate(
{ _id },
{
'$push': {
'pauses': { '$each': [ pauses[1] ], '$position': 0 }
}
},
{ 'new': true, 'upsert': true }
);
log(tracker);
tracker = await AltTracker.findOneAndUpdate(
{ _id, 'pauses.0.end': { '$exists': false } },
{ '$set': { 'pauses.0.end': new Date() } },
{ 'new': true }
);
log(tracker);
})()
} catch(e) {
console.error(e)
} finally {
mongoose.disconnect()
}
})()
And the output demonstrating what happens on all updates:
Mongoose: trackers.deleteMany({}, {})
Mongoose: alttrackers.deleteMany({}, {})
Mongoose: trackers.insertOne({ pauses: [ { reason: 'Lunch', start: new Date("Wed, 21 Nov 2018 18:13:22 GMT"), end: new Date("Wed, 21 Nov 2018 18:14:30 GMT") }, { reason: 'Lunch', start: new Date("Wed, 21 Nov 2018 18:15:09 GMT") } ], _id: ObjectId("5bf65cf6ae7b8639c3f5090d"), __v: 0 })
{
"pauses": [
{
"reason": "Lunch",
"start": "2018-11-21T18:13:22.835Z",
"end": "2018-11-21T18:14:30.835Z"
},
{
"reason": "Lunch",
"start": "2018-11-21T18:15:09.057Z"
}
],
"_id": "5bf65cf6ae7b8639c3f5090d",
"__v": 0
}
{
"reason": "Lunch",
"start": "2018-11-21T18:15:09.057Z",
"end": "2018-11-22T07:38:30.883Z"
}
Mongoose: trackers.updateOne({ _id: ObjectId("5bf65cf6ae7b8639c3f5090d"), __v: 0 }, { '$set': { pauses: [ { reason: 'Lunch', start: new Date("Wed, 21 Nov 2018 18:13:22 GMT"), end: new Date("Wed, 21 Nov 2018 18:14:30 GMT") }, { reason: 'Lunch', start: new Date("Wed, 21 Nov 2018 18:15:09 GMT"), end: new Date("Thu, 22 Nov 2018 07:38:30 GMT") } ] }, '$inc': { __v: 1 } })
Mongoose: trackers.findOne({ _id: ObjectId("5bf65cf6ae7b8639c3f5090d") }, { projection: {} })
{
"pauses": [
{
"reason": "Lunch",
"start": "2018-11-21T18:13:22.835Z",
"end": "2018-11-21T18:14:30.835Z"
},
{
"reason": "Lunch",
"start": "2018-11-21T18:15:09.057Z",
"end": "2018-11-22T07:38:30.883Z"
}
],
"_id": "5bf65cf6ae7b8639c3f5090d",
"__v": 1
}
Mongoose: alttrackers.insertOne({ _id: ObjectId("5bf65cf6ae7b8639c3f5090e"), pauses: [ { _id: ObjectId("5bf65cf6ae7b8639c3f50910"), reason: 'Lunch', start: new Date("Wed, 21 Nov 2018 18:13:22 GMT"), end: new Date("Wed, 21 Nov 2018 18:14:30 GMT") }, { _id: ObjectId("5bf65cf6ae7b8639c3f5090f"), reason: 'Lunch', start: new Date("Wed, 21 Nov 2018 18:15:09 GMT") } ], __v: 0 })
{
"_id": "5bf65cf6ae7b8639c3f5090e",
"pauses": [
{
"_id": "5bf65cf6ae7b8639c3f50910",
"reason": "Lunch",
"start": "2018-11-21T18:13:22.835Z",
"end": "2018-11-21T18:14:30.835Z"
},
{
"_id": "5bf65cf6ae7b8639c3f5090f",
"reason": "Lunch",
"start": "2018-11-21T18:15:09.057Z"
}
],
"__v": 0
}
{
"_id": "5bf65cf6ae7b8639c3f5090f",
"reason": "Lunch",
"start": "2018-11-21T18:15:09.057Z",
"end": "2018-11-22T07:38:30.915Z"
}
Mongoose: alttrackers.updateOne({ _id: ObjectId("5bf65cf6ae7b8639c3f5090e"), __v: 0 }, { '$set': { 'pauses.1.end': new Date("Thu, 22 Nov 2018 07:38:30 GMT") } })
Mongoose: alttrackers.findOne({ _id: ObjectId("5bf65cf6ae7b8639c3f5090e") }, { projection: {} })
{
"_id": "5bf65cf6ae7b8639c3f5090e",
"pauses": [
{
"_id": "5bf65cf6ae7b8639c3f50910",
"reason": "Lunch",
"start": "2018-11-21T18:13:22.835Z",
"end": "2018-11-21T18:14:30.835Z"
},
{
"_id": "5bf65cf6ae7b8639c3f5090f",
"reason": "Lunch",
"start": "2018-11-21T18:15:09.057Z",
"end": "2018-11-22T07:38:30.915Z"
}
],
"__v": 0
}
Mongoose: alttrackers.findOneAndUpdate({ _id: ObjectId("5bf65cf6ae7b8639c3f50911") }, { '$setOnInsert': { __v: 0 }, '$push': { pauses: { '$each': [ { _id: ObjectId("5bf65cf6ae7b8639c3f50913"), reason: 'Lunch', start: new Date("Wed, 21 Nov 2018 18:13:22 GMT"), end: new Date("Wed, 21 Nov 2018 18:14:30 GMT") }, { _id: ObjectId("5bf65cf6ae7b8639c3f50912"), reason: 'Lunch', start: new Date("Wed, 21 Nov 2018 18:15:09 GMT") } ], '$sort': { start: -1 } } } }, { upsert: true, remove: false, projection: {}, returnOriginal: false })
{
"_id": "5bf65cf6ae7b8639c3f50911",
"__v": 0,
"pauses": [
{
"_id": "5bf65cf6ae7b8639c3f50912",
"reason": "Lunch",
"start": "2018-11-21T18:15:09.057Z"
},
{
"_id": "5bf65cf6ae7b8639c3f50913",
"reason": "Lunch",
"start": "2018-11-21T18:13:22.835Z",
"end": "2018-11-21T18:14:30.835Z"
}
]
}
Mongoose: alttrackers.findOneAndUpdate({ _id: ObjectId("5bf65cf6ae7b8639c3f50911"), 'pauses.0.end': { '$exists': false } }, { '$set': { 'pauses.0.end': new Date("Thu, 22 Nov 2018 07:38:30 GMT") } }, { upsert: false, remove: false, projection: {}, returnOriginal: false })
{
"_id": "5bf65cf6ae7b8639c3f50911",
"__v": 0,
"pauses": [
{
"_id": "5bf65cf6ae7b8639c3f50912",
"reason": "Lunch",
"start": "2018-11-21T18:15:09.057Z",
"end": "2018-11-22T07:38:30.940Z"
},
{
"_id": "5bf65cf6ae7b8639c3f50913",
"reason": "Lunch",
"start": "2018-11-21T18:13:22.835Z",
"end": "2018-11-21T18:14:30.835Z"
}
]
}
Mongoose: alttrackers.findOneAndUpdate({ _id: ObjectId("5bf65cf6ae7b8639c3f50914") }, { '$setOnInsert': { __v: 0 }, '$push': { pauses: { '$each': [ { _id: ObjectId("5bf65cf6ae7b8639c3f50915"), reason: 'Lunch', start: new Date("Wed, 21 Nov 2018 18:13:22 GMT"), end: new Date("Wed, 21 Nov 2018 18:14:30 GMT") } ], '$position': 0 } } }, { upsert: true, remove: false, projection: {}, returnOriginal: false })
{
"_id": "5bf65cf6ae7b8639c3f50914",
"__v": 0,
"pauses": [
{
"_id": "5bf65cf6ae7b8639c3f50915",
"reason": "Lunch",
"start": "2018-11-21T18:13:22.835Z",
"end": "2018-11-21T18:14:30.835Z"
}
]
}
Mongoose: alttrackers.findOneAndUpdate({ _id: ObjectId("5bf65cf6ae7b8639c3f50914") }, { '$setOnInsert': { __v: 0 }, '$push': { pauses: { '$each': [ { _id: ObjectId("5bf65cf6ae7b8639c3f50916"), reason: 'Lunch', start: new Date("Wed, 21 Nov 2018 18:15:09 GMT") } ], '$position': 0 } } }, { upsert: true, remove: false, projection: {}, returnOriginal: false })
{
"_id": "5bf65cf6ae7b8639c3f50914",
"__v": 0,
"pauses": [
{
"_id": "5bf65cf6ae7b8639c3f50916",
"reason": "Lunch",
"start": "2018-11-21T18:15:09.057Z"
},
{
"_id": "5bf65cf6ae7b8639c3f50915",
"reason": "Lunch",
"start": "2018-11-21T18:13:22.835Z",
"end": "2018-11-21T18:14:30.835Z"
}
]
}
Mongoose: alttrackers.findOneAndUpdate({ _id: ObjectId("5bf65cf6ae7b8639c3f50914"), 'pauses.0.end': { '$exists': false } }, { '$set': { 'pauses.0.end': new Date("Thu, 22 Nov 2018 07:38:30 GMT") } }, { upsert: false, remove: false, projection: {}, returnOriginal: false })
{
"_id": "5bf65cf6ae7b8639c3f50914",
"__v": 0,
"pauses": [
{
"_id": "5bf65cf6ae7b8639c3f50916",
"reason": "Lunch",
"start": "2018-11-21T18:15:09.057Z",
"end": "2018-11-22T07:38:30.957Z"
},
{
"_id": "5bf65cf6ae7b8639c3f50915",
"reason": "Lunch",
"start": "2018-11-21T18:13:22.835Z",
"end": "2018-11-21T18:14:30.835Z"
}
]
}
1
Thank you very much. Your answer not only helps me to solve the problem but also gives me understanding of the matter.
– Felix
Nov 22 '18 at 19:33
add a comment |
Schema Problems
The basic reason this is not updating for you is because Mongoose actually demands to be told that such changes actually did something. This is essentially related to how it keeps internal references and then decides what to send back to the server as an update()
statement when you actually call save()
.
The "simple" fix for this is to just call the markModified()
method on the path before you save()
:
tracker.markModified('pauses');
tracker.save((error) => {
There is however another way, since the only reason you ever need to call markModified()
is because mongoose has no concept of what you actually changed on the underlying element. Typically people encounter this on the Mixed
type. But in your case you defined Array
on pauses
without specifying he structure of the schema for the items within that array.
Therefore simply adding schema allows the changes without calling markModified()
// pauses: Array // change this line
pauses: [{
reason: String,
start: Date,
end: Date
}]
Once you do either of those the changes to the array element will actually be persisted, but as you note the whole process of findOne()
and then making these changes and calling save()
is not really ideal.
As such, there is a way to to this atomically, but it does require you make some changes.
Atomic Updates
The core problem here is updating the last element of the array. MongoDB does not have any real method for either querying or updating the last element in any efficient manner. This is not a new problem, but the common line is that the solution to this has been around for some time.
Reverse Sort the array
The basic principle is that as you add items to your array you can actually atomically sort all of the elements in the array by a given property so that there is always a certain array order. The reason this is a reverse sort is that what MongoDB is actually very happy with is the first array element.
Therefore in order to keep the array elements ordered so the latest is always the first element, you can apply the $sort
modifier to $push
as follows:
vat newPauses = [{ reason: "Next", start: new Date() }];
Tracker.findOneAndUpdate(
{ _id },
{
'$push': {
'pauses': { '$each': newPauses, '$sort': { start: -1 } }
}
},
{ new: true }
)
That is what your code would look like every time you want to add to the array. Because the new item has a more recent date value in start
the $sort
modifier will re-arrange he array so that this most recent item is actually at the beginning.
You can even update all existing documents in your collection with one simple statement:
Tracker.updateMany(
{},
{
'$push': {
'pauses': { '$each': , '$sort': { start: -1 } }
}
}
)
In this case the $each
is given an empty array, so of course no new items are added to the arrays in any document. But the $sort
is fired and all items will then be re-ordered by their start time.
This method using $sort
suits a lot of people, but there may be cases where it is not the right solution. This is why there is another way.
Prepend to the array
The general case here is where you consider that either you don't really have a property such as the start
which should govern what the order of elements in the array actually is, or even in cases where you don't want to have any possible overhead of applying a $sort
or move existing elements around in any way.
In order to achieve this all that is needed is to add the $position
modifier to your $push
statement:
vat newPauses = [{ reason: "Next", start: new Date() }];
Tracker.findOneAndUpdate(
{ _id },
{
'$push': {
'pauses': { '$each': newPauses, '$position': 0 }
}
},
{ new: true }
)
In short, $position
tells MongoDB where the new element is supposed to go by array index. The 0
of course means the beginning of the array, and this will move all existing members along to the right rather than adding a new member to the right side.
The only drawback here is that you cannot really update the existing array members in a purely reversed order without running a process to loop through all documents and rewrite them in the collection as there is no atomic update that can reverse the whole array in the same way the $sort
can modify the existing array. So the setup cost if changing to using $position
is a little higher.
If you want this type of approach, then the methods for updating your existing collection are extensively documented at Update MongoDB field using value of another field
Demonstration
Of course these are best described by demonstration. Here is a full listing demonstrating all approaches:
const { Schema, Types: { ObjectId } } = mongoose = require('mongoose');
const uri = 'mongodb://localhost:27017/trackdemo';
const opts = { useNewUrlParser: true };
// sensible defaults
mongoose.Promise = global.Promise;
mongoose.set('debug', true);
mongoose.set('useFindAndModify', false);
mongoose.set('useCreateIndex', true);
// schema defs
const pauseSchema = new Schema({
reason: String,
start: Date,
end: Date
})
const trackerSchema = new Schema({
pauses: Array
});
const altTrackerSchema = new Schema({
pauses: [pauseSchema]
});
const Tracker = mongoose.model('Tracker', trackerSchema);
const AltTracker = mongoose.model('AltTracker', altTrackerSchema);
// log helper
const log = data => console.log(JSON.stringify(data, undefined, 2));
// main
const getPauses = () => [
{
reason: "Lunch",
start: new Date("2018-11-21T18:13:22.835Z"),
end: new Date("2018-11-21T18:14:30.835Z")
},
{
reason: "Lunch",
start: new Date("2018-11-21T18:15:09.057Z")
}
];
(async function() {
try {
const conn = await mongoose.connect(uri, opts);
// Clean models
await Promise.all(
Object.entries(conn.models).map(([k,m]) => m.deleteMany())
);
// Test Tracker
await (async function() {
let pauses = getPauses();
let tracker = await Tracker.create({ pauses });
log(tracker);
let temp = tracker.pauses[tracker.pauses.length - 1];
temp.end = new Date();
log(temp);
tracker.pauses[tracker.pauses.length -1] = temp;
tracker.markModified('pauses');
await tracker.save();
let result = await Tracker.findById(tracker._id);
log(result);
})()
// Test AltTracker
await (async function() {
let pauses = getPauses();
let tracker = await AltTracker.create({ pauses });
log(tracker);
let temp = tracker.pauses[tracker.pauses.length - 1];
temp.end = new Date();
log(temp);
tracker.pauses[tracker.pauses.length -1] = temp;
//tracker.markModified('pauses');
await tracker.save();
let result = await AltTracker.findById(tracker._id);
log(result);
})()
// AltTracker atomic $sort method
await (async function() {
let _id = new ObjectId(); // keep ref for tests
let pauses = getPauses();
let tracker = await AltTracker.findOneAndUpdate(
{ _id },
{
'$push': {
'pauses': { '$each': pauses, '$sort': { 'start': -1 } }
}
},
{ 'new': true, 'upsert': true }
);
log(tracker);
// update first
tracker = await AltTracker.findOneAndUpdate(
{ _id, 'pauses.0.end': { '$exists': false } },
{ '$set': { 'pauses.0.end': new Date() } },
{ 'new': true }
);
log(tracker);
})()
// AltTracker atomic $position method
await (async function() {
let _id = new ObjectId(); // keep ref for tests
let pauses = getPauses();
// Doing this twice purely for demo
let tracker = await AltTracker.findOneAndUpdate(
{ _id },
{
'$push': {
'pauses': { '$each': [ pauses[0] ], '$position': 0 }
}
},
{ 'new': true, 'upsert': true }
);
log(tracker);
tracker = await AltTracker.findOneAndUpdate(
{ _id },
{
'$push': {
'pauses': { '$each': [ pauses[1] ], '$position': 0 }
}
},
{ 'new': true, 'upsert': true }
);
log(tracker);
tracker = await AltTracker.findOneAndUpdate(
{ _id, 'pauses.0.end': { '$exists': false } },
{ '$set': { 'pauses.0.end': new Date() } },
{ 'new': true }
);
log(tracker);
})()
} catch(e) {
console.error(e)
} finally {
mongoose.disconnect()
}
})()
And the output demonstrating what happens on all updates:
Mongoose: trackers.deleteMany({}, {})
Mongoose: alttrackers.deleteMany({}, {})
Mongoose: trackers.insertOne({ pauses: [ { reason: 'Lunch', start: new Date("Wed, 21 Nov 2018 18:13:22 GMT"), end: new Date("Wed, 21 Nov 2018 18:14:30 GMT") }, { reason: 'Lunch', start: new Date("Wed, 21 Nov 2018 18:15:09 GMT") } ], _id: ObjectId("5bf65cf6ae7b8639c3f5090d"), __v: 0 })
{
"pauses": [
{
"reason": "Lunch",
"start": "2018-11-21T18:13:22.835Z",
"end": "2018-11-21T18:14:30.835Z"
},
{
"reason": "Lunch",
"start": "2018-11-21T18:15:09.057Z"
}
],
"_id": "5bf65cf6ae7b8639c3f5090d",
"__v": 0
}
{
"reason": "Lunch",
"start": "2018-11-21T18:15:09.057Z",
"end": "2018-11-22T07:38:30.883Z"
}
Mongoose: trackers.updateOne({ _id: ObjectId("5bf65cf6ae7b8639c3f5090d"), __v: 0 }, { '$set': { pauses: [ { reason: 'Lunch', start: new Date("Wed, 21 Nov 2018 18:13:22 GMT"), end: new Date("Wed, 21 Nov 2018 18:14:30 GMT") }, { reason: 'Lunch', start: new Date("Wed, 21 Nov 2018 18:15:09 GMT"), end: new Date("Thu, 22 Nov 2018 07:38:30 GMT") } ] }, '$inc': { __v: 1 } })
Mongoose: trackers.findOne({ _id: ObjectId("5bf65cf6ae7b8639c3f5090d") }, { projection: {} })
{
"pauses": [
{
"reason": "Lunch",
"start": "2018-11-21T18:13:22.835Z",
"end": "2018-11-21T18:14:30.835Z"
},
{
"reason": "Lunch",
"start": "2018-11-21T18:15:09.057Z",
"end": "2018-11-22T07:38:30.883Z"
}
],
"_id": "5bf65cf6ae7b8639c3f5090d",
"__v": 1
}
Mongoose: alttrackers.insertOne({ _id: ObjectId("5bf65cf6ae7b8639c3f5090e"), pauses: [ { _id: ObjectId("5bf65cf6ae7b8639c3f50910"), reason: 'Lunch', start: new Date("Wed, 21 Nov 2018 18:13:22 GMT"), end: new Date("Wed, 21 Nov 2018 18:14:30 GMT") }, { _id: ObjectId("5bf65cf6ae7b8639c3f5090f"), reason: 'Lunch', start: new Date("Wed, 21 Nov 2018 18:15:09 GMT") } ], __v: 0 })
{
"_id": "5bf65cf6ae7b8639c3f5090e",
"pauses": [
{
"_id": "5bf65cf6ae7b8639c3f50910",
"reason": "Lunch",
"start": "2018-11-21T18:13:22.835Z",
"end": "2018-11-21T18:14:30.835Z"
},
{
"_id": "5bf65cf6ae7b8639c3f5090f",
"reason": "Lunch",
"start": "2018-11-21T18:15:09.057Z"
}
],
"__v": 0
}
{
"_id": "5bf65cf6ae7b8639c3f5090f",
"reason": "Lunch",
"start": "2018-11-21T18:15:09.057Z",
"end": "2018-11-22T07:38:30.915Z"
}
Mongoose: alttrackers.updateOne({ _id: ObjectId("5bf65cf6ae7b8639c3f5090e"), __v: 0 }, { '$set': { 'pauses.1.end': new Date("Thu, 22 Nov 2018 07:38:30 GMT") } })
Mongoose: alttrackers.findOne({ _id: ObjectId("5bf65cf6ae7b8639c3f5090e") }, { projection: {} })
{
"_id": "5bf65cf6ae7b8639c3f5090e",
"pauses": [
{
"_id": "5bf65cf6ae7b8639c3f50910",
"reason": "Lunch",
"start": "2018-11-21T18:13:22.835Z",
"end": "2018-11-21T18:14:30.835Z"
},
{
"_id": "5bf65cf6ae7b8639c3f5090f",
"reason": "Lunch",
"start": "2018-11-21T18:15:09.057Z",
"end": "2018-11-22T07:38:30.915Z"
}
],
"__v": 0
}
Mongoose: alttrackers.findOneAndUpdate({ _id: ObjectId("5bf65cf6ae7b8639c3f50911") }, { '$setOnInsert': { __v: 0 }, '$push': { pauses: { '$each': [ { _id: ObjectId("5bf65cf6ae7b8639c3f50913"), reason: 'Lunch', start: new Date("Wed, 21 Nov 2018 18:13:22 GMT"), end: new Date("Wed, 21 Nov 2018 18:14:30 GMT") }, { _id: ObjectId("5bf65cf6ae7b8639c3f50912"), reason: 'Lunch', start: new Date("Wed, 21 Nov 2018 18:15:09 GMT") } ], '$sort': { start: -1 } } } }, { upsert: true, remove: false, projection: {}, returnOriginal: false })
{
"_id": "5bf65cf6ae7b8639c3f50911",
"__v": 0,
"pauses": [
{
"_id": "5bf65cf6ae7b8639c3f50912",
"reason": "Lunch",
"start": "2018-11-21T18:15:09.057Z"
},
{
"_id": "5bf65cf6ae7b8639c3f50913",
"reason": "Lunch",
"start": "2018-11-21T18:13:22.835Z",
"end": "2018-11-21T18:14:30.835Z"
}
]
}
Mongoose: alttrackers.findOneAndUpdate({ _id: ObjectId("5bf65cf6ae7b8639c3f50911"), 'pauses.0.end': { '$exists': false } }, { '$set': { 'pauses.0.end': new Date("Thu, 22 Nov 2018 07:38:30 GMT") } }, { upsert: false, remove: false, projection: {}, returnOriginal: false })
{
"_id": "5bf65cf6ae7b8639c3f50911",
"__v": 0,
"pauses": [
{
"_id": "5bf65cf6ae7b8639c3f50912",
"reason": "Lunch",
"start": "2018-11-21T18:15:09.057Z",
"end": "2018-11-22T07:38:30.940Z"
},
{
"_id": "5bf65cf6ae7b8639c3f50913",
"reason": "Lunch",
"start": "2018-11-21T18:13:22.835Z",
"end": "2018-11-21T18:14:30.835Z"
}
]
}
Mongoose: alttrackers.findOneAndUpdate({ _id: ObjectId("5bf65cf6ae7b8639c3f50914") }, { '$setOnInsert': { __v: 0 }, '$push': { pauses: { '$each': [ { _id: ObjectId("5bf65cf6ae7b8639c3f50915"), reason: 'Lunch', start: new Date("Wed, 21 Nov 2018 18:13:22 GMT"), end: new Date("Wed, 21 Nov 2018 18:14:30 GMT") } ], '$position': 0 } } }, { upsert: true, remove: false, projection: {}, returnOriginal: false })
{
"_id": "5bf65cf6ae7b8639c3f50914",
"__v": 0,
"pauses": [
{
"_id": "5bf65cf6ae7b8639c3f50915",
"reason": "Lunch",
"start": "2018-11-21T18:13:22.835Z",
"end": "2018-11-21T18:14:30.835Z"
}
]
}
Mongoose: alttrackers.findOneAndUpdate({ _id: ObjectId("5bf65cf6ae7b8639c3f50914") }, { '$setOnInsert': { __v: 0 }, '$push': { pauses: { '$each': [ { _id: ObjectId("5bf65cf6ae7b8639c3f50916"), reason: 'Lunch', start: new Date("Wed, 21 Nov 2018 18:15:09 GMT") } ], '$position': 0 } } }, { upsert: true, remove: false, projection: {}, returnOriginal: false })
{
"_id": "5bf65cf6ae7b8639c3f50914",
"__v": 0,
"pauses": [
{
"_id": "5bf65cf6ae7b8639c3f50916",
"reason": "Lunch",
"start": "2018-11-21T18:15:09.057Z"
},
{
"_id": "5bf65cf6ae7b8639c3f50915",
"reason": "Lunch",
"start": "2018-11-21T18:13:22.835Z",
"end": "2018-11-21T18:14:30.835Z"
}
]
}
Mongoose: alttrackers.findOneAndUpdate({ _id: ObjectId("5bf65cf6ae7b8639c3f50914"), 'pauses.0.end': { '$exists': false } }, { '$set': { 'pauses.0.end': new Date("Thu, 22 Nov 2018 07:38:30 GMT") } }, { upsert: false, remove: false, projection: {}, returnOriginal: false })
{
"_id": "5bf65cf6ae7b8639c3f50914",
"__v": 0,
"pauses": [
{
"_id": "5bf65cf6ae7b8639c3f50916",
"reason": "Lunch",
"start": "2018-11-21T18:15:09.057Z",
"end": "2018-11-22T07:38:30.957Z"
},
{
"_id": "5bf65cf6ae7b8639c3f50915",
"reason": "Lunch",
"start": "2018-11-21T18:13:22.835Z",
"end": "2018-11-21T18:14:30.835Z"
}
]
}
Schema Problems
The basic reason this is not updating for you is because Mongoose actually demands to be told that such changes actually did something. This is essentially related to how it keeps internal references and then decides what to send back to the server as an update()
statement when you actually call save()
.
The "simple" fix for this is to just call the markModified()
method on the path before you save()
:
tracker.markModified('pauses');
tracker.save((error) => {
There is however another way, since the only reason you ever need to call markModified()
is because mongoose has no concept of what you actually changed on the underlying element. Typically people encounter this on the Mixed
type. But in your case you defined Array
on pauses
without specifying he structure of the schema for the items within that array.
Therefore simply adding schema allows the changes without calling markModified()
// pauses: Array // change this line
pauses: [{
reason: String,
start: Date,
end: Date
}]
Once you do either of those the changes to the array element will actually be persisted, but as you note the whole process of findOne()
and then making these changes and calling save()
is not really ideal.
As such, there is a way to to this atomically, but it does require you make some changes.
Atomic Updates
The core problem here is updating the last element of the array. MongoDB does not have any real method for either querying or updating the last element in any efficient manner. This is not a new problem, but the common line is that the solution to this has been around for some time.
Reverse Sort the array
The basic principle is that as you add items to your array you can actually atomically sort all of the elements in the array by a given property so that there is always a certain array order. The reason this is a reverse sort is that what MongoDB is actually very happy with is the first array element.
Therefore in order to keep the array elements ordered so the latest is always the first element, you can apply the $sort
modifier to $push
as follows:
vat newPauses = [{ reason: "Next", start: new Date() }];
Tracker.findOneAndUpdate(
{ _id },
{
'$push': {
'pauses': { '$each': newPauses, '$sort': { start: -1 } }
}
},
{ new: true }
)
That is what your code would look like every time you want to add to the array. Because the new item has a more recent date value in start
the $sort
modifier will re-arrange he array so that this most recent item is actually at the beginning.
You can even update all existing documents in your collection with one simple statement:
Tracker.updateMany(
{},
{
'$push': {
'pauses': { '$each': , '$sort': { start: -1 } }
}
}
)
In this case the $each
is given an empty array, so of course no new items are added to the arrays in any document. But the $sort
is fired and all items will then be re-ordered by their start time.
This method using $sort
suits a lot of people, but there may be cases where it is not the right solution. This is why there is another way.
Prepend to the array
The general case here is where you consider that either you don't really have a property such as the start
which should govern what the order of elements in the array actually is, or even in cases where you don't want to have any possible overhead of applying a $sort
or move existing elements around in any way.
In order to achieve this all that is needed is to add the $position
modifier to your $push
statement:
vat newPauses = [{ reason: "Next", start: new Date() }];
Tracker.findOneAndUpdate(
{ _id },
{
'$push': {
'pauses': { '$each': newPauses, '$position': 0 }
}
},
{ new: true }
)
In short, $position
tells MongoDB where the new element is supposed to go by array index. The 0
of course means the beginning of the array, and this will move all existing members along to the right rather than adding a new member to the right side.
The only drawback here is that you cannot really update the existing array members in a purely reversed order without running a process to loop through all documents and rewrite them in the collection as there is no atomic update that can reverse the whole array in the same way the $sort
can modify the existing array. So the setup cost if changing to using $position
is a little higher.
If you want this type of approach, then the methods for updating your existing collection are extensively documented at Update MongoDB field using value of another field
Demonstration
Of course these are best described by demonstration. Here is a full listing demonstrating all approaches:
const { Schema, Types: { ObjectId } } = mongoose = require('mongoose');
const uri = 'mongodb://localhost:27017/trackdemo';
const opts = { useNewUrlParser: true };
// sensible defaults
mongoose.Promise = global.Promise;
mongoose.set('debug', true);
mongoose.set('useFindAndModify', false);
mongoose.set('useCreateIndex', true);
// schema defs
const pauseSchema = new Schema({
reason: String,
start: Date,
end: Date
})
const trackerSchema = new Schema({
pauses: Array
});
const altTrackerSchema = new Schema({
pauses: [pauseSchema]
});
const Tracker = mongoose.model('Tracker', trackerSchema);
const AltTracker = mongoose.model('AltTracker', altTrackerSchema);
// log helper
const log = data => console.log(JSON.stringify(data, undefined, 2));
// main
const getPauses = () => [
{
reason: "Lunch",
start: new Date("2018-11-21T18:13:22.835Z"),
end: new Date("2018-11-21T18:14:30.835Z")
},
{
reason: "Lunch",
start: new Date("2018-11-21T18:15:09.057Z")
}
];
(async function() {
try {
const conn = await mongoose.connect(uri, opts);
// Clean models
await Promise.all(
Object.entries(conn.models).map(([k,m]) => m.deleteMany())
);
// Test Tracker
await (async function() {
let pauses = getPauses();
let tracker = await Tracker.create({ pauses });
log(tracker);
let temp = tracker.pauses[tracker.pauses.length - 1];
temp.end = new Date();
log(temp);
tracker.pauses[tracker.pauses.length -1] = temp;
tracker.markModified('pauses');
await tracker.save();
let result = await Tracker.findById(tracker._id);
log(result);
})()
// Test AltTracker
await (async function() {
let pauses = getPauses();
let tracker = await AltTracker.create({ pauses });
log(tracker);
let temp = tracker.pauses[tracker.pauses.length - 1];
temp.end = new Date();
log(temp);
tracker.pauses[tracker.pauses.length -1] = temp;
//tracker.markModified('pauses');
await tracker.save();
let result = await AltTracker.findById(tracker._id);
log(result);
})()
// AltTracker atomic $sort method
await (async function() {
let _id = new ObjectId(); // keep ref for tests
let pauses = getPauses();
let tracker = await AltTracker.findOneAndUpdate(
{ _id },
{
'$push': {
'pauses': { '$each': pauses, '$sort': { 'start': -1 } }
}
},
{ 'new': true, 'upsert': true }
);
log(tracker);
// update first
tracker = await AltTracker.findOneAndUpdate(
{ _id, 'pauses.0.end': { '$exists': false } },
{ '$set': { 'pauses.0.end': new Date() } },
{ 'new': true }
);
log(tracker);
})()
// AltTracker atomic $position method
await (async function() {
let _id = new ObjectId(); // keep ref for tests
let pauses = getPauses();
// Doing this twice purely for demo
let tracker = await AltTracker.findOneAndUpdate(
{ _id },
{
'$push': {
'pauses': { '$each': [ pauses[0] ], '$position': 0 }
}
},
{ 'new': true, 'upsert': true }
);
log(tracker);
tracker = await AltTracker.findOneAndUpdate(
{ _id },
{
'$push': {
'pauses': { '$each': [ pauses[1] ], '$position': 0 }
}
},
{ 'new': true, 'upsert': true }
);
log(tracker);
tracker = await AltTracker.findOneAndUpdate(
{ _id, 'pauses.0.end': { '$exists': false } },
{ '$set': { 'pauses.0.end': new Date() } },
{ 'new': true }
);
log(tracker);
})()
} catch(e) {
console.error(e)
} finally {
mongoose.disconnect()
}
})()
And the output demonstrating what happens on all updates:
Mongoose: trackers.deleteMany({}, {})
Mongoose: alttrackers.deleteMany({}, {})
Mongoose: trackers.insertOne({ pauses: [ { reason: 'Lunch', start: new Date("Wed, 21 Nov 2018 18:13:22 GMT"), end: new Date("Wed, 21 Nov 2018 18:14:30 GMT") }, { reason: 'Lunch', start: new Date("Wed, 21 Nov 2018 18:15:09 GMT") } ], _id: ObjectId("5bf65cf6ae7b8639c3f5090d"), __v: 0 })
{
"pauses": [
{
"reason": "Lunch",
"start": "2018-11-21T18:13:22.835Z",
"end": "2018-11-21T18:14:30.835Z"
},
{
"reason": "Lunch",
"start": "2018-11-21T18:15:09.057Z"
}
],
"_id": "5bf65cf6ae7b8639c3f5090d",
"__v": 0
}
{
"reason": "Lunch",
"start": "2018-11-21T18:15:09.057Z",
"end": "2018-11-22T07:38:30.883Z"
}
Mongoose: trackers.updateOne({ _id: ObjectId("5bf65cf6ae7b8639c3f5090d"), __v: 0 }, { '$set': { pauses: [ { reason: 'Lunch', start: new Date("Wed, 21 Nov 2018 18:13:22 GMT"), end: new Date("Wed, 21 Nov 2018 18:14:30 GMT") }, { reason: 'Lunch', start: new Date("Wed, 21 Nov 2018 18:15:09 GMT"), end: new Date("Thu, 22 Nov 2018 07:38:30 GMT") } ] }, '$inc': { __v: 1 } })
Mongoose: trackers.findOne({ _id: ObjectId("5bf65cf6ae7b8639c3f5090d") }, { projection: {} })
{
"pauses": [
{
"reason": "Lunch",
"start": "2018-11-21T18:13:22.835Z",
"end": "2018-11-21T18:14:30.835Z"
},
{
"reason": "Lunch",
"start": "2018-11-21T18:15:09.057Z",
"end": "2018-11-22T07:38:30.883Z"
}
],
"_id": "5bf65cf6ae7b8639c3f5090d",
"__v": 1
}
Mongoose: alttrackers.insertOne({ _id: ObjectId("5bf65cf6ae7b8639c3f5090e"), pauses: [ { _id: ObjectId("5bf65cf6ae7b8639c3f50910"), reason: 'Lunch', start: new Date("Wed, 21 Nov 2018 18:13:22 GMT"), end: new Date("Wed, 21 Nov 2018 18:14:30 GMT") }, { _id: ObjectId("5bf65cf6ae7b8639c3f5090f"), reason: 'Lunch', start: new Date("Wed, 21 Nov 2018 18:15:09 GMT") } ], __v: 0 })
{
"_id": "5bf65cf6ae7b8639c3f5090e",
"pauses": [
{
"_id": "5bf65cf6ae7b8639c3f50910",
"reason": "Lunch",
"start": "2018-11-21T18:13:22.835Z",
"end": "2018-11-21T18:14:30.835Z"
},
{
"_id": "5bf65cf6ae7b8639c3f5090f",
"reason": "Lunch",
"start": "2018-11-21T18:15:09.057Z"
}
],
"__v": 0
}
{
"_id": "5bf65cf6ae7b8639c3f5090f",
"reason": "Lunch",
"start": "2018-11-21T18:15:09.057Z",
"end": "2018-11-22T07:38:30.915Z"
}
Mongoose: alttrackers.updateOne({ _id: ObjectId("5bf65cf6ae7b8639c3f5090e"), __v: 0 }, { '$set': { 'pauses.1.end': new Date("Thu, 22 Nov 2018 07:38:30 GMT") } })
Mongoose: alttrackers.findOne({ _id: ObjectId("5bf65cf6ae7b8639c3f5090e") }, { projection: {} })
{
"_id": "5bf65cf6ae7b8639c3f5090e",
"pauses": [
{
"_id": "5bf65cf6ae7b8639c3f50910",
"reason": "Lunch",
"start": "2018-11-21T18:13:22.835Z",
"end": "2018-11-21T18:14:30.835Z"
},
{
"_id": "5bf65cf6ae7b8639c3f5090f",
"reason": "Lunch",
"start": "2018-11-21T18:15:09.057Z",
"end": "2018-11-22T07:38:30.915Z"
}
],
"__v": 0
}
Mongoose: alttrackers.findOneAndUpdate({ _id: ObjectId("5bf65cf6ae7b8639c3f50911") }, { '$setOnInsert': { __v: 0 }, '$push': { pauses: { '$each': [ { _id: ObjectId("5bf65cf6ae7b8639c3f50913"), reason: 'Lunch', start: new Date("Wed, 21 Nov 2018 18:13:22 GMT"), end: new Date("Wed, 21 Nov 2018 18:14:30 GMT") }, { _id: ObjectId("5bf65cf6ae7b8639c3f50912"), reason: 'Lunch', start: new Date("Wed, 21 Nov 2018 18:15:09 GMT") } ], '$sort': { start: -1 } } } }, { upsert: true, remove: false, projection: {}, returnOriginal: false })
{
"_id": "5bf65cf6ae7b8639c3f50911",
"__v": 0,
"pauses": [
{
"_id": "5bf65cf6ae7b8639c3f50912",
"reason": "Lunch",
"start": "2018-11-21T18:15:09.057Z"
},
{
"_id": "5bf65cf6ae7b8639c3f50913",
"reason": "Lunch",
"start": "2018-11-21T18:13:22.835Z",
"end": "2018-11-21T18:14:30.835Z"
}
]
}
Mongoose: alttrackers.findOneAndUpdate({ _id: ObjectId("5bf65cf6ae7b8639c3f50911"), 'pauses.0.end': { '$exists': false } }, { '$set': { 'pauses.0.end': new Date("Thu, 22 Nov 2018 07:38:30 GMT") } }, { upsert: false, remove: false, projection: {}, returnOriginal: false })
{
"_id": "5bf65cf6ae7b8639c3f50911",
"__v": 0,
"pauses": [
{
"_id": "5bf65cf6ae7b8639c3f50912",
"reason": "Lunch",
"start": "2018-11-21T18:15:09.057Z",
"end": "2018-11-22T07:38:30.940Z"
},
{
"_id": "5bf65cf6ae7b8639c3f50913",
"reason": "Lunch",
"start": "2018-11-21T18:13:22.835Z",
"end": "2018-11-21T18:14:30.835Z"
}
]
}
Mongoose: alttrackers.findOneAndUpdate({ _id: ObjectId("5bf65cf6ae7b8639c3f50914") }, { '$setOnInsert': { __v: 0 }, '$push': { pauses: { '$each': [ { _id: ObjectId("5bf65cf6ae7b8639c3f50915"), reason: 'Lunch', start: new Date("Wed, 21 Nov 2018 18:13:22 GMT"), end: new Date("Wed, 21 Nov 2018 18:14:30 GMT") } ], '$position': 0 } } }, { upsert: true, remove: false, projection: {}, returnOriginal: false })
{
"_id": "5bf65cf6ae7b8639c3f50914",
"__v": 0,
"pauses": [
{
"_id": "5bf65cf6ae7b8639c3f50915",
"reason": "Lunch",
"start": "2018-11-21T18:13:22.835Z",
"end": "2018-11-21T18:14:30.835Z"
}
]
}
Mongoose: alttrackers.findOneAndUpdate({ _id: ObjectId("5bf65cf6ae7b8639c3f50914") }, { '$setOnInsert': { __v: 0 }, '$push': { pauses: { '$each': [ { _id: ObjectId("5bf65cf6ae7b8639c3f50916"), reason: 'Lunch', start: new Date("Wed, 21 Nov 2018 18:15:09 GMT") } ], '$position': 0 } } }, { upsert: true, remove: false, projection: {}, returnOriginal: false })
{
"_id": "5bf65cf6ae7b8639c3f50914",
"__v": 0,
"pauses": [
{
"_id": "5bf65cf6ae7b8639c3f50916",
"reason": "Lunch",
"start": "2018-11-21T18:15:09.057Z"
},
{
"_id": "5bf65cf6ae7b8639c3f50915",
"reason": "Lunch",
"start": "2018-11-21T18:13:22.835Z",
"end": "2018-11-21T18:14:30.835Z"
}
]
}
Mongoose: alttrackers.findOneAndUpdate({ _id: ObjectId("5bf65cf6ae7b8639c3f50914"), 'pauses.0.end': { '$exists': false } }, { '$set': { 'pauses.0.end': new Date("Thu, 22 Nov 2018 07:38:30 GMT") } }, { upsert: false, remove: false, projection: {}, returnOriginal: false })
{
"_id": "5bf65cf6ae7b8639c3f50914",
"__v": 0,
"pauses": [
{
"_id": "5bf65cf6ae7b8639c3f50916",
"reason": "Lunch",
"start": "2018-11-21T18:15:09.057Z",
"end": "2018-11-22T07:38:30.957Z"
},
{
"_id": "5bf65cf6ae7b8639c3f50915",
"reason": "Lunch",
"start": "2018-11-21T18:13:22.835Z",
"end": "2018-11-21T18:14:30.835Z"
}
]
}
answered Nov 22 '18 at 7:46
Neil LunnNeil Lunn
98.2k23174184
98.2k23174184
1
Thank you very much. Your answer not only helps me to solve the problem but also gives me understanding of the matter.
– Felix
Nov 22 '18 at 19:33
add a comment |
1
Thank you very much. Your answer not only helps me to solve the problem but also gives me understanding of the matter.
– Felix
Nov 22 '18 at 19:33
1
1
Thank you very much. Your answer not only helps me to solve the problem but also gives me understanding of the matter.
– Felix
Nov 22 '18 at 19:33
Thank you very much. Your answer not only helps me to solve the problem but also gives me understanding of the matter.
– Felix
Nov 22 '18 at 19:33
add a comment |
Thanks for contributing an answer to Stack Overflow!
- Please be sure to answer the question. Provide details and share your research!
But avoid …
- Asking for help, clarification, or responding to other answers.
- Making statements based on opinion; back them up with references or personal experience.
To learn more, see our tips on writing great answers.
Sign up or log in
StackExchange.ready(function () {
StackExchange.helpers.onClickDraftSave('#login-link');
});
Sign up using Google
Sign up using Facebook
Sign up using Email and Password
Post as a guest
Required, but never shown
StackExchange.ready(
function () {
StackExchange.openid.initPostLogin('.new-post-login', 'https%3a%2f%2fstackoverflow.com%2fquestions%2f53418669%2fupdate-last-item-in-array%23new-answer', 'question_page');
}
);
Post as a guest
Required, but never shown
Sign up or log in
StackExchange.ready(function () {
StackExchange.helpers.onClickDraftSave('#login-link');
});
Sign up using Google
Sign up using Facebook
Sign up using Email and Password
Post as a guest
Required, but never shown
Sign up or log in
StackExchange.ready(function () {
StackExchange.helpers.onClickDraftSave('#login-link');
});
Sign up using Google
Sign up using Facebook
Sign up using Email and Password
Post as a guest
Required, but never shown
Sign up or log in
StackExchange.ready(function () {
StackExchange.helpers.onClickDraftSave('#login-link');
});
Sign up using Google
Sign up using Facebook
Sign up using Email and Password
Sign up using Google
Sign up using Facebook
Sign up using Email and Password
Post as a guest
Required, but never shown
Required, but never shown
Required, but never shown
Required, but never shown
Required, but never shown
Required, but never shown
Required, but never shown
Required, but never shown
Required, but never shown