Update last item in array












3















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









share|improve this question





























    3















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









    share|improve this question



























      3












      3








      3


      1






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









      share|improve this question
















      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






      share|improve this question















      share|improve this question













      share|improve this question




      share|improve this question








      edited Nov 22 '18 at 8:14









      Raza Rafaideen

      5731519




      5731519










      asked Nov 21 '18 at 18:44









      FelixFelix

      306




      306
























          1 Answer
          1






          active

          oldest

          votes


















          2














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





          share|improve this answer



















          • 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











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


          }
          });














          draft saved

          draft discarded


















          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









          2














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





          share|improve this answer



















          • 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
















          2














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





          share|improve this answer



















          • 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














          2












          2








          2







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





          share|improve this answer













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






          share|improve this answer












          share|improve this answer



          share|improve this answer










          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














          • 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




















          draft saved

          draft discarded




















































          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.




          draft saved


          draft discarded














          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





















































          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







          Popular posts from this blog

          "Incorrect syntax near the keyword 'ON'. (on update cascade, on delete cascade,)

          Alcedinidae

          RAC Tourist Trophy