Mongodb change the order of an array











up vote
0
down vote

favorite
1












I am stumped on this one. I have an images array with in my collection, the users can rearrange the order of images on the client side and I am trying to save the new order to the database. The imagesOrder array is the new images in the new order and it only has the url so I want to match the url to the urls in the database. I am not sure how to make the index a variable or if this is possible:



this is what I have so far. my code editor shows and error on [index] so I know that is not the proper format but not sure what is:



imagesOrder.forEach((index, image) => {
const imageUrl = image.url
const index = index
Users.update({
id
}, {
$set: {
images[index]: imageUrl
}
})
});









share|improve this question


























    up vote
    0
    down vote

    favorite
    1












    I am stumped on this one. I have an images array with in my collection, the users can rearrange the order of images on the client side and I am trying to save the new order to the database. The imagesOrder array is the new images in the new order and it only has the url so I want to match the url to the urls in the database. I am not sure how to make the index a variable or if this is possible:



    this is what I have so far. my code editor shows and error on [index] so I know that is not the proper format but not sure what is:



    imagesOrder.forEach((index, image) => {
    const imageUrl = image.url
    const index = index
    Users.update({
    id
    }, {
    $set: {
    images[index]: imageUrl
    }
    })
    });









    share|improve this question
























      up vote
      0
      down vote

      favorite
      1









      up vote
      0
      down vote

      favorite
      1






      1





      I am stumped on this one. I have an images array with in my collection, the users can rearrange the order of images on the client side and I am trying to save the new order to the database. The imagesOrder array is the new images in the new order and it only has the url so I want to match the url to the urls in the database. I am not sure how to make the index a variable or if this is possible:



      this is what I have so far. my code editor shows and error on [index] so I know that is not the proper format but not sure what is:



      imagesOrder.forEach((index, image) => {
      const imageUrl = image.url
      const index = index
      Users.update({
      id
      }, {
      $set: {
      images[index]: imageUrl
      }
      })
      });









      share|improve this question













      I am stumped on this one. I have an images array with in my collection, the users can rearrange the order of images on the client side and I am trying to save the new order to the database. The imagesOrder array is the new images in the new order and it only has the url so I want to match the url to the urls in the database. I am not sure how to make the index a variable or if this is possible:



      this is what I have so far. my code editor shows and error on [index] so I know that is not the proper format but not sure what is:



      imagesOrder.forEach((index, image) => {
      const imageUrl = image.url
      const index = index
      Users.update({
      id
      }, {
      $set: {
      images[index]: imageUrl
      }
      })
      });






      javascript mongodb






      share|improve this question













      share|improve this question











      share|improve this question




      share|improve this question










      asked Nov 21 at 23:55









      Kris

      9110




      9110
























          1 Answer
          1






          active

          oldest

          votes

















          up vote
          1
          down vote



          accepted










          So that is not actually the way you would do this. Basically there is no need to actually send an update request to the server for every single indexed position for the array. Also the update() method is asynchronous, so it's not something you ever put inside a forEach() which does not respect awaiting the completion of an asynchronous call.



          Instead what is usually the most practical solution is to just $set the entire array content in one request. Also mocking up your imagesOrder to something practical since forEach() even actually has the signature of .forEach((<element>,><index>) => ..., which seems different to what you were expecting given the code in the question.



           var imagesOrder = [
          { index: 0, url: '/one' }, { index: 1, url: '/two' }, { index: 2, url: '/three' }
          ];

          let response = await Users.updateOne(
          { id },
          { "$set": { "images": imagesOrder.map(({ url }) => url) } }
          );

          // { "$set": { "images": ["/one","/two","/three"] } }


          Much like the forEach() a map() does the same array iteration but with the difference that it actually returns an array generated by the processing function. This is actually what you want since all that is needed here is to simply extract the values of the url property from each object.



          Note the index properties are actually already in order and really redundant here, but I'm just approximating what it sounds like you have from your question. Since an "array" actually maintains it's own order then such a property "should" be redundant and it would be advisable that your source array data actually conforms to this.



          If however you managed to record such index values in a way they are actually out of order, then the best solution is to add a sort():



           var imagesOrder = [
          { index: 2, url: '/three' }, { index: 0, url: '/one' }, { index: 1, url: '/two' }
          ];

          let response = await Users.updateOne(
          { id },
          { "$set": {
          "images": imagesOrder.sort((a,b) => a.index - b.index)
          .map(({ url }) => url)
          }}
          );

          // { "$set": { "images": ["/one","/two","/three"] } }




          As for what you "attempted", it's not really benefiting you in any way to actually attempt updating each element at a given position. But if you really wanted to see it done, then again you actually would just instead build up a single update request:



           var imagesOrder = [
          { index: 2, url: '/three' }, { index: 0, url: '/one' }, { index: 1, url: '/two' }
          ];

          var update = { $set: {} };

          for ( let { url, index } of imagesOrder.sort((a,b) => a.index - b.index) ) {
          update.$set['images.'+index] = url;
          }

          /*
          * Creates:
          *
          * { "$set": {
          * "images.0": "/one",
          * "images.1": "/two",
          * "images.2": "/three"
          * }}
          */

          let response = await Users.updateOne({ id }, update);


          Or in the case where the index property was not there or irrelevant since the array is already ordered:



           var imagesOrder = [
          { index: 2, url: '/three' }, { index: 0, url: '/one' }, { index: 1, url: '/two' }
          ];

          for ( let [index, { url }] of Object.entries(imagesOrder) ) {
          update.$set['images.'+index] = url;
          }

          /*
          * Creates:
          *
          * { "$set": {
          * "images.0": "/one",
          * "images.1": "/two",
          * "images.2": "/three"
          * }}
          */

          let response = await Users.updateOne({ id }, update);


          So it's all pretty much the same thing. Note the common form of notation is actually a "string" for the key which includes the index position numerically. This is described in Dot Notation within the core documentation for the MongoDB query language.



          The one main difference here is that should your new array contain more entries than the actual array stored in the document to be modified, that second form using the "dot notation" to the indexed position is going to fail since it cannot "set" an index position which does not exist.



          For this reason even though there are other pitfalls to "replacing" the array as the original examples show, it's a lot safer than attempting to update via the positional index in the stored document.





          Note that this should be enough to have you at least started in the right direction. Making this work with multiple users possibly updating the data at once can become pretty complicated in terms of update statements for both checking and merging changes.



          In most cases the simple "replace" will be more than adequate at least for a while. And of course the main lesson here should be to not loop "async" methods in places where it is completely unnecessary. Most of the time what you really want to "loop" is the construction of the statement, if of course any looping is required at all and most of the time it really isn't.





          Addendum



          Just in case you or anyone had it in mind to actually store an array of objects with the index position values stored within them, this can become a little more complex, but it can also serve as an example of how to actually issue an update statement which does not "replace" the array and actually is safe considering it does not rely on indexed positions of the array but instead using matching conditions.



          This is possible with the positional filtered $[<identifier>] syntax introduced in MongoDB 3.6. This allows conditions to specify which element to update ( i.e by matching url ) instead of including the index positions within the statement directly. It's safer since if no matching element is found, then the syntax allows for not attempting to change anything at all.



          Also as demonstration the method to $sort the elements based on updated index values is shown. Noting this actually uses the $push modifier even though in this statement we are not actually adding anything to the array. Just reordering the elements. But it's how you actually do that atomically:



          const { Schema, Types: { ObjectId } } = mongoose = require('mongoose');

          const uri = 'mongodb://localhost:27017/longorder';
          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 imageSchema = new Schema({
          index: Number,
          url: String
          })

          const userSchema = new Schema({
          images: [imageSchema]
          });

          const User = mongoose.model('User', userSchema);

          // log helper

          const log = data => console.log(JSON.stringify(data, undefined, 2));

          (async function() {

          try {

          const conn = await mongoose.connect(uri, opts);

          // clean models
          await Promise.all(
          Object.entries(conn.models).map(([k,m]) => m.deleteMany())
          );

          // Create data

          let _id = new ObjectId();

          let user = await User.findOneAndUpdate(
          { _id },
          {
          '$push': {
          'images': {
          '$each': [
          { index: 2, url: '/one' },
          { index: 0, url: '/three' },
          { index: 1, url: '/two' }
          ],
          '$sort': { 'index': 1 }
          }
          }
          },
          { 'new': true, 'upsert': true }
          );

          log(user);

          // Change order request
          let orderImages = [
          { index: 2, url: '/three' },
          { index: 0, url: '/one' },
          { index: 1, url: '/two' }
          ];

          let $set = { };
          let arrayFilters = ;

          for ( { index, url } of orderImages ) {
          let key = url.replace(/^//,'');
          arrayFilters.push({ [`${key}.url`]: url });
          $set[`images.$[${key}].index`] = index;
          }

          let ops = [
          // Update the index value of each matching item
          { 'updateOne': {
          'filter': { _id },
          'update': { $set },
          arrayFilters
          }},
          // Re-sort the array by index value
          { 'updateOne': {
          'filter': { _id },
          'update': {
          '$push': {
          'images': { '$each': , '$sort': { 'index': 1 } }
          }
          }
          }}
          ];

          log(ops);

          let response = await User.bulkWrite(ops);
          log(response);

          let newuser = await User.findOne({ _id });
          log(newuser);


          } catch(e) {
          console.error(e)
          } finally {
          mongoose.disconnect()
          }

          })()


          And the output, showing initial document state, the update and actual changes made:



          Mongoose: users.deleteMany({}, {})
          Mongoose: users.findOneAndUpdate({ _id: ObjectId("5bf6116621293f2ab3dec3d3") }, { '$setOnInsert': { __v: 0 }, '$push': { images: { '$each': [ { _id: ObjectId("5bf6116621293f2ab3dec3d6"), index: 2, url: '/one' }, { _id: ObjectId("5bf6116621293f2ab3dec3d5"), index: 0, url: '/three' }, { _id: ObjectId("5bf6116621293f2ab3dec3d4"), index: 1, url: '/two' } ], '$sort': { index: 1 } } } }, { upsert: true, remove: false, projection: {}, returnOriginal: false })
          {
          "_id": "5bf6116621293f2ab3dec3d3",
          "__v": 0,
          "images": [
          {
          "_id": "5bf6116621293f2ab3dec3d5",
          "index": 0,
          "url": "/three"
          },
          {
          "_id": "5bf6116621293f2ab3dec3d4",
          "index": 1,
          "url": "/two"
          },
          {
          "_id": "5bf6116621293f2ab3dec3d6",
          "index": 2,
          "url": "/one"
          }
          ]
          }
          [
          {
          "updateOne": {
          "filter": {
          "_id": "5bf6116621293f2ab3dec3d3"
          },
          "update": {
          "$set": {
          "images.$[three].index": 2,
          "images.$[one].index": 0,
          "images.$[two].index": 1
          }
          },
          "arrayFilters": [
          {
          "three.url": "/three"
          },
          {
          "one.url": "/one"
          },
          {
          "two.url": "/two"
          }
          ]
          }
          },
          {
          "updateOne": {
          "filter": {
          "_id": "5bf6116621293f2ab3dec3d3"
          },
          "update": {
          "$push": {
          "images": {
          "$each": ,
          "$sort": {
          "index": 1
          }
          }
          }
          }
          }
          }
          ]
          Mongoose: users.bulkWrite([ { updateOne: { filter: { _id: 5bf6116621293f2ab3dec3d3 }, update: { '$set': { 'images.$[three].index': 2, 'images.$[one].index': 0, 'images.$[two].index': 1 } }, arrayFilters: [ { 'three.url': '/three' }, { 'one.url': '/one' }, { 'two.url': '/two' } ] } }, { updateOne: { filter: { _id: 5bf6116621293f2ab3dec3d3 }, update: { '$push': { images: { '$each': , '$sort': { index: 1 } } } } } } ], {})
          {
          "ok": 1,
          "writeErrors": ,
          "writeConcernErrors": ,
          "insertedIds": ,
          "nInserted": 0,
          "nUpserted": 0,
          "nMatched": 2,
          "nModified": 2,
          "nRemoved": 0,
          "upserted": ,
          "lastOp": {
          "ts": "6626503031506599940",
          "t": 139
          }
          }
          Mongoose: users.findOne({ _id: ObjectId("5bf6116621293f2ab3dec3d3") }, { projection: {} })
          {
          "_id": "5bf6116621293f2ab3dec3d3",
          "__v": 0,
          "images": [
          {
          "_id": "5bf6116621293f2ab3dec3d6",
          "index": 0,
          "url": "/one"
          },
          {
          "_id": "5bf6116621293f2ab3dec3d4",
          "index": 1,
          "url": "/two"
          },
          {
          "_id": "5bf6116621293f2ab3dec3d5",
          "index": 2,
          "url": "/three"
          }
          ]
          }





          share|improve this answer























          • Thank you for this great explanation. This is super helpful!
            – Kris
            2 days ago











          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',
          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%2f53422086%2fmongodb-change-the-order-of-an-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








          up vote
          1
          down vote



          accepted










          So that is not actually the way you would do this. Basically there is no need to actually send an update request to the server for every single indexed position for the array. Also the update() method is asynchronous, so it's not something you ever put inside a forEach() which does not respect awaiting the completion of an asynchronous call.



          Instead what is usually the most practical solution is to just $set the entire array content in one request. Also mocking up your imagesOrder to something practical since forEach() even actually has the signature of .forEach((<element>,><index>) => ..., which seems different to what you were expecting given the code in the question.



           var imagesOrder = [
          { index: 0, url: '/one' }, { index: 1, url: '/two' }, { index: 2, url: '/three' }
          ];

          let response = await Users.updateOne(
          { id },
          { "$set": { "images": imagesOrder.map(({ url }) => url) } }
          );

          // { "$set": { "images": ["/one","/two","/three"] } }


          Much like the forEach() a map() does the same array iteration but with the difference that it actually returns an array generated by the processing function. This is actually what you want since all that is needed here is to simply extract the values of the url property from each object.



          Note the index properties are actually already in order and really redundant here, but I'm just approximating what it sounds like you have from your question. Since an "array" actually maintains it's own order then such a property "should" be redundant and it would be advisable that your source array data actually conforms to this.



          If however you managed to record such index values in a way they are actually out of order, then the best solution is to add a sort():



           var imagesOrder = [
          { index: 2, url: '/three' }, { index: 0, url: '/one' }, { index: 1, url: '/two' }
          ];

          let response = await Users.updateOne(
          { id },
          { "$set": {
          "images": imagesOrder.sort((a,b) => a.index - b.index)
          .map(({ url }) => url)
          }}
          );

          // { "$set": { "images": ["/one","/two","/three"] } }




          As for what you "attempted", it's not really benefiting you in any way to actually attempt updating each element at a given position. But if you really wanted to see it done, then again you actually would just instead build up a single update request:



           var imagesOrder = [
          { index: 2, url: '/three' }, { index: 0, url: '/one' }, { index: 1, url: '/two' }
          ];

          var update = { $set: {} };

          for ( let { url, index } of imagesOrder.sort((a,b) => a.index - b.index) ) {
          update.$set['images.'+index] = url;
          }

          /*
          * Creates:
          *
          * { "$set": {
          * "images.0": "/one",
          * "images.1": "/two",
          * "images.2": "/three"
          * }}
          */

          let response = await Users.updateOne({ id }, update);


          Or in the case where the index property was not there or irrelevant since the array is already ordered:



           var imagesOrder = [
          { index: 2, url: '/three' }, { index: 0, url: '/one' }, { index: 1, url: '/two' }
          ];

          for ( let [index, { url }] of Object.entries(imagesOrder) ) {
          update.$set['images.'+index] = url;
          }

          /*
          * Creates:
          *
          * { "$set": {
          * "images.0": "/one",
          * "images.1": "/two",
          * "images.2": "/three"
          * }}
          */

          let response = await Users.updateOne({ id }, update);


          So it's all pretty much the same thing. Note the common form of notation is actually a "string" for the key which includes the index position numerically. This is described in Dot Notation within the core documentation for the MongoDB query language.



          The one main difference here is that should your new array contain more entries than the actual array stored in the document to be modified, that second form using the "dot notation" to the indexed position is going to fail since it cannot "set" an index position which does not exist.



          For this reason even though there are other pitfalls to "replacing" the array as the original examples show, it's a lot safer than attempting to update via the positional index in the stored document.





          Note that this should be enough to have you at least started in the right direction. Making this work with multiple users possibly updating the data at once can become pretty complicated in terms of update statements for both checking and merging changes.



          In most cases the simple "replace" will be more than adequate at least for a while. And of course the main lesson here should be to not loop "async" methods in places where it is completely unnecessary. Most of the time what you really want to "loop" is the construction of the statement, if of course any looping is required at all and most of the time it really isn't.





          Addendum



          Just in case you or anyone had it in mind to actually store an array of objects with the index position values stored within them, this can become a little more complex, but it can also serve as an example of how to actually issue an update statement which does not "replace" the array and actually is safe considering it does not rely on indexed positions of the array but instead using matching conditions.



          This is possible with the positional filtered $[<identifier>] syntax introduced in MongoDB 3.6. This allows conditions to specify which element to update ( i.e by matching url ) instead of including the index positions within the statement directly. It's safer since if no matching element is found, then the syntax allows for not attempting to change anything at all.



          Also as demonstration the method to $sort the elements based on updated index values is shown. Noting this actually uses the $push modifier even though in this statement we are not actually adding anything to the array. Just reordering the elements. But it's how you actually do that atomically:



          const { Schema, Types: { ObjectId } } = mongoose = require('mongoose');

          const uri = 'mongodb://localhost:27017/longorder';
          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 imageSchema = new Schema({
          index: Number,
          url: String
          })

          const userSchema = new Schema({
          images: [imageSchema]
          });

          const User = mongoose.model('User', userSchema);

          // log helper

          const log = data => console.log(JSON.stringify(data, undefined, 2));

          (async function() {

          try {

          const conn = await mongoose.connect(uri, opts);

          // clean models
          await Promise.all(
          Object.entries(conn.models).map(([k,m]) => m.deleteMany())
          );

          // Create data

          let _id = new ObjectId();

          let user = await User.findOneAndUpdate(
          { _id },
          {
          '$push': {
          'images': {
          '$each': [
          { index: 2, url: '/one' },
          { index: 0, url: '/three' },
          { index: 1, url: '/two' }
          ],
          '$sort': { 'index': 1 }
          }
          }
          },
          { 'new': true, 'upsert': true }
          );

          log(user);

          // Change order request
          let orderImages = [
          { index: 2, url: '/three' },
          { index: 0, url: '/one' },
          { index: 1, url: '/two' }
          ];

          let $set = { };
          let arrayFilters = ;

          for ( { index, url } of orderImages ) {
          let key = url.replace(/^//,'');
          arrayFilters.push({ [`${key}.url`]: url });
          $set[`images.$[${key}].index`] = index;
          }

          let ops = [
          // Update the index value of each matching item
          { 'updateOne': {
          'filter': { _id },
          'update': { $set },
          arrayFilters
          }},
          // Re-sort the array by index value
          { 'updateOne': {
          'filter': { _id },
          'update': {
          '$push': {
          'images': { '$each': , '$sort': { 'index': 1 } }
          }
          }
          }}
          ];

          log(ops);

          let response = await User.bulkWrite(ops);
          log(response);

          let newuser = await User.findOne({ _id });
          log(newuser);


          } catch(e) {
          console.error(e)
          } finally {
          mongoose.disconnect()
          }

          })()


          And the output, showing initial document state, the update and actual changes made:



          Mongoose: users.deleteMany({}, {})
          Mongoose: users.findOneAndUpdate({ _id: ObjectId("5bf6116621293f2ab3dec3d3") }, { '$setOnInsert': { __v: 0 }, '$push': { images: { '$each': [ { _id: ObjectId("5bf6116621293f2ab3dec3d6"), index: 2, url: '/one' }, { _id: ObjectId("5bf6116621293f2ab3dec3d5"), index: 0, url: '/three' }, { _id: ObjectId("5bf6116621293f2ab3dec3d4"), index: 1, url: '/two' } ], '$sort': { index: 1 } } } }, { upsert: true, remove: false, projection: {}, returnOriginal: false })
          {
          "_id": "5bf6116621293f2ab3dec3d3",
          "__v": 0,
          "images": [
          {
          "_id": "5bf6116621293f2ab3dec3d5",
          "index": 0,
          "url": "/three"
          },
          {
          "_id": "5bf6116621293f2ab3dec3d4",
          "index": 1,
          "url": "/two"
          },
          {
          "_id": "5bf6116621293f2ab3dec3d6",
          "index": 2,
          "url": "/one"
          }
          ]
          }
          [
          {
          "updateOne": {
          "filter": {
          "_id": "5bf6116621293f2ab3dec3d3"
          },
          "update": {
          "$set": {
          "images.$[three].index": 2,
          "images.$[one].index": 0,
          "images.$[two].index": 1
          }
          },
          "arrayFilters": [
          {
          "three.url": "/three"
          },
          {
          "one.url": "/one"
          },
          {
          "two.url": "/two"
          }
          ]
          }
          },
          {
          "updateOne": {
          "filter": {
          "_id": "5bf6116621293f2ab3dec3d3"
          },
          "update": {
          "$push": {
          "images": {
          "$each": ,
          "$sort": {
          "index": 1
          }
          }
          }
          }
          }
          }
          ]
          Mongoose: users.bulkWrite([ { updateOne: { filter: { _id: 5bf6116621293f2ab3dec3d3 }, update: { '$set': { 'images.$[three].index': 2, 'images.$[one].index': 0, 'images.$[two].index': 1 } }, arrayFilters: [ { 'three.url': '/three' }, { 'one.url': '/one' }, { 'two.url': '/two' } ] } }, { updateOne: { filter: { _id: 5bf6116621293f2ab3dec3d3 }, update: { '$push': { images: { '$each': , '$sort': { index: 1 } } } } } } ], {})
          {
          "ok": 1,
          "writeErrors": ,
          "writeConcernErrors": ,
          "insertedIds": ,
          "nInserted": 0,
          "nUpserted": 0,
          "nMatched": 2,
          "nModified": 2,
          "nRemoved": 0,
          "upserted": ,
          "lastOp": {
          "ts": "6626503031506599940",
          "t": 139
          }
          }
          Mongoose: users.findOne({ _id: ObjectId("5bf6116621293f2ab3dec3d3") }, { projection: {} })
          {
          "_id": "5bf6116621293f2ab3dec3d3",
          "__v": 0,
          "images": [
          {
          "_id": "5bf6116621293f2ab3dec3d6",
          "index": 0,
          "url": "/one"
          },
          {
          "_id": "5bf6116621293f2ab3dec3d4",
          "index": 1,
          "url": "/two"
          },
          {
          "_id": "5bf6116621293f2ab3dec3d5",
          "index": 2,
          "url": "/three"
          }
          ]
          }





          share|improve this answer























          • Thank you for this great explanation. This is super helpful!
            – Kris
            2 days ago















          up vote
          1
          down vote



          accepted










          So that is not actually the way you would do this. Basically there is no need to actually send an update request to the server for every single indexed position for the array. Also the update() method is asynchronous, so it's not something you ever put inside a forEach() which does not respect awaiting the completion of an asynchronous call.



          Instead what is usually the most practical solution is to just $set the entire array content in one request. Also mocking up your imagesOrder to something practical since forEach() even actually has the signature of .forEach((<element>,><index>) => ..., which seems different to what you were expecting given the code in the question.



           var imagesOrder = [
          { index: 0, url: '/one' }, { index: 1, url: '/two' }, { index: 2, url: '/three' }
          ];

          let response = await Users.updateOne(
          { id },
          { "$set": { "images": imagesOrder.map(({ url }) => url) } }
          );

          // { "$set": { "images": ["/one","/two","/three"] } }


          Much like the forEach() a map() does the same array iteration but with the difference that it actually returns an array generated by the processing function. This is actually what you want since all that is needed here is to simply extract the values of the url property from each object.



          Note the index properties are actually already in order and really redundant here, but I'm just approximating what it sounds like you have from your question. Since an "array" actually maintains it's own order then such a property "should" be redundant and it would be advisable that your source array data actually conforms to this.



          If however you managed to record such index values in a way they are actually out of order, then the best solution is to add a sort():



           var imagesOrder = [
          { index: 2, url: '/three' }, { index: 0, url: '/one' }, { index: 1, url: '/two' }
          ];

          let response = await Users.updateOne(
          { id },
          { "$set": {
          "images": imagesOrder.sort((a,b) => a.index - b.index)
          .map(({ url }) => url)
          }}
          );

          // { "$set": { "images": ["/one","/two","/three"] } }




          As for what you "attempted", it's not really benefiting you in any way to actually attempt updating each element at a given position. But if you really wanted to see it done, then again you actually would just instead build up a single update request:



           var imagesOrder = [
          { index: 2, url: '/three' }, { index: 0, url: '/one' }, { index: 1, url: '/two' }
          ];

          var update = { $set: {} };

          for ( let { url, index } of imagesOrder.sort((a,b) => a.index - b.index) ) {
          update.$set['images.'+index] = url;
          }

          /*
          * Creates:
          *
          * { "$set": {
          * "images.0": "/one",
          * "images.1": "/two",
          * "images.2": "/three"
          * }}
          */

          let response = await Users.updateOne({ id }, update);


          Or in the case where the index property was not there or irrelevant since the array is already ordered:



           var imagesOrder = [
          { index: 2, url: '/three' }, { index: 0, url: '/one' }, { index: 1, url: '/two' }
          ];

          for ( let [index, { url }] of Object.entries(imagesOrder) ) {
          update.$set['images.'+index] = url;
          }

          /*
          * Creates:
          *
          * { "$set": {
          * "images.0": "/one",
          * "images.1": "/two",
          * "images.2": "/three"
          * }}
          */

          let response = await Users.updateOne({ id }, update);


          So it's all pretty much the same thing. Note the common form of notation is actually a "string" for the key which includes the index position numerically. This is described in Dot Notation within the core documentation for the MongoDB query language.



          The one main difference here is that should your new array contain more entries than the actual array stored in the document to be modified, that second form using the "dot notation" to the indexed position is going to fail since it cannot "set" an index position which does not exist.



          For this reason even though there are other pitfalls to "replacing" the array as the original examples show, it's a lot safer than attempting to update via the positional index in the stored document.





          Note that this should be enough to have you at least started in the right direction. Making this work with multiple users possibly updating the data at once can become pretty complicated in terms of update statements for both checking and merging changes.



          In most cases the simple "replace" will be more than adequate at least for a while. And of course the main lesson here should be to not loop "async" methods in places where it is completely unnecessary. Most of the time what you really want to "loop" is the construction of the statement, if of course any looping is required at all and most of the time it really isn't.





          Addendum



          Just in case you or anyone had it in mind to actually store an array of objects with the index position values stored within them, this can become a little more complex, but it can also serve as an example of how to actually issue an update statement which does not "replace" the array and actually is safe considering it does not rely on indexed positions of the array but instead using matching conditions.



          This is possible with the positional filtered $[<identifier>] syntax introduced in MongoDB 3.6. This allows conditions to specify which element to update ( i.e by matching url ) instead of including the index positions within the statement directly. It's safer since if no matching element is found, then the syntax allows for not attempting to change anything at all.



          Also as demonstration the method to $sort the elements based on updated index values is shown. Noting this actually uses the $push modifier even though in this statement we are not actually adding anything to the array. Just reordering the elements. But it's how you actually do that atomically:



          const { Schema, Types: { ObjectId } } = mongoose = require('mongoose');

          const uri = 'mongodb://localhost:27017/longorder';
          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 imageSchema = new Schema({
          index: Number,
          url: String
          })

          const userSchema = new Schema({
          images: [imageSchema]
          });

          const User = mongoose.model('User', userSchema);

          // log helper

          const log = data => console.log(JSON.stringify(data, undefined, 2));

          (async function() {

          try {

          const conn = await mongoose.connect(uri, opts);

          // clean models
          await Promise.all(
          Object.entries(conn.models).map(([k,m]) => m.deleteMany())
          );

          // Create data

          let _id = new ObjectId();

          let user = await User.findOneAndUpdate(
          { _id },
          {
          '$push': {
          'images': {
          '$each': [
          { index: 2, url: '/one' },
          { index: 0, url: '/three' },
          { index: 1, url: '/two' }
          ],
          '$sort': { 'index': 1 }
          }
          }
          },
          { 'new': true, 'upsert': true }
          );

          log(user);

          // Change order request
          let orderImages = [
          { index: 2, url: '/three' },
          { index: 0, url: '/one' },
          { index: 1, url: '/two' }
          ];

          let $set = { };
          let arrayFilters = ;

          for ( { index, url } of orderImages ) {
          let key = url.replace(/^//,'');
          arrayFilters.push({ [`${key}.url`]: url });
          $set[`images.$[${key}].index`] = index;
          }

          let ops = [
          // Update the index value of each matching item
          { 'updateOne': {
          'filter': { _id },
          'update': { $set },
          arrayFilters
          }},
          // Re-sort the array by index value
          { 'updateOne': {
          'filter': { _id },
          'update': {
          '$push': {
          'images': { '$each': , '$sort': { 'index': 1 } }
          }
          }
          }}
          ];

          log(ops);

          let response = await User.bulkWrite(ops);
          log(response);

          let newuser = await User.findOne({ _id });
          log(newuser);


          } catch(e) {
          console.error(e)
          } finally {
          mongoose.disconnect()
          }

          })()


          And the output, showing initial document state, the update and actual changes made:



          Mongoose: users.deleteMany({}, {})
          Mongoose: users.findOneAndUpdate({ _id: ObjectId("5bf6116621293f2ab3dec3d3") }, { '$setOnInsert': { __v: 0 }, '$push': { images: { '$each': [ { _id: ObjectId("5bf6116621293f2ab3dec3d6"), index: 2, url: '/one' }, { _id: ObjectId("5bf6116621293f2ab3dec3d5"), index: 0, url: '/three' }, { _id: ObjectId("5bf6116621293f2ab3dec3d4"), index: 1, url: '/two' } ], '$sort': { index: 1 } } } }, { upsert: true, remove: false, projection: {}, returnOriginal: false })
          {
          "_id": "5bf6116621293f2ab3dec3d3",
          "__v": 0,
          "images": [
          {
          "_id": "5bf6116621293f2ab3dec3d5",
          "index": 0,
          "url": "/three"
          },
          {
          "_id": "5bf6116621293f2ab3dec3d4",
          "index": 1,
          "url": "/two"
          },
          {
          "_id": "5bf6116621293f2ab3dec3d6",
          "index": 2,
          "url": "/one"
          }
          ]
          }
          [
          {
          "updateOne": {
          "filter": {
          "_id": "5bf6116621293f2ab3dec3d3"
          },
          "update": {
          "$set": {
          "images.$[three].index": 2,
          "images.$[one].index": 0,
          "images.$[two].index": 1
          }
          },
          "arrayFilters": [
          {
          "three.url": "/three"
          },
          {
          "one.url": "/one"
          },
          {
          "two.url": "/two"
          }
          ]
          }
          },
          {
          "updateOne": {
          "filter": {
          "_id": "5bf6116621293f2ab3dec3d3"
          },
          "update": {
          "$push": {
          "images": {
          "$each": ,
          "$sort": {
          "index": 1
          }
          }
          }
          }
          }
          }
          ]
          Mongoose: users.bulkWrite([ { updateOne: { filter: { _id: 5bf6116621293f2ab3dec3d3 }, update: { '$set': { 'images.$[three].index': 2, 'images.$[one].index': 0, 'images.$[two].index': 1 } }, arrayFilters: [ { 'three.url': '/three' }, { 'one.url': '/one' }, { 'two.url': '/two' } ] } }, { updateOne: { filter: { _id: 5bf6116621293f2ab3dec3d3 }, update: { '$push': { images: { '$each': , '$sort': { index: 1 } } } } } } ], {})
          {
          "ok": 1,
          "writeErrors": ,
          "writeConcernErrors": ,
          "insertedIds": ,
          "nInserted": 0,
          "nUpserted": 0,
          "nMatched": 2,
          "nModified": 2,
          "nRemoved": 0,
          "upserted": ,
          "lastOp": {
          "ts": "6626503031506599940",
          "t": 139
          }
          }
          Mongoose: users.findOne({ _id: ObjectId("5bf6116621293f2ab3dec3d3") }, { projection: {} })
          {
          "_id": "5bf6116621293f2ab3dec3d3",
          "__v": 0,
          "images": [
          {
          "_id": "5bf6116621293f2ab3dec3d6",
          "index": 0,
          "url": "/one"
          },
          {
          "_id": "5bf6116621293f2ab3dec3d4",
          "index": 1,
          "url": "/two"
          },
          {
          "_id": "5bf6116621293f2ab3dec3d5",
          "index": 2,
          "url": "/three"
          }
          ]
          }





          share|improve this answer























          • Thank you for this great explanation. This is super helpful!
            – Kris
            2 days ago













          up vote
          1
          down vote



          accepted







          up vote
          1
          down vote



          accepted






          So that is not actually the way you would do this. Basically there is no need to actually send an update request to the server for every single indexed position for the array. Also the update() method is asynchronous, so it's not something you ever put inside a forEach() which does not respect awaiting the completion of an asynchronous call.



          Instead what is usually the most practical solution is to just $set the entire array content in one request. Also mocking up your imagesOrder to something practical since forEach() even actually has the signature of .forEach((<element>,><index>) => ..., which seems different to what you were expecting given the code in the question.



           var imagesOrder = [
          { index: 0, url: '/one' }, { index: 1, url: '/two' }, { index: 2, url: '/three' }
          ];

          let response = await Users.updateOne(
          { id },
          { "$set": { "images": imagesOrder.map(({ url }) => url) } }
          );

          // { "$set": { "images": ["/one","/two","/three"] } }


          Much like the forEach() a map() does the same array iteration but with the difference that it actually returns an array generated by the processing function. This is actually what you want since all that is needed here is to simply extract the values of the url property from each object.



          Note the index properties are actually already in order and really redundant here, but I'm just approximating what it sounds like you have from your question. Since an "array" actually maintains it's own order then such a property "should" be redundant and it would be advisable that your source array data actually conforms to this.



          If however you managed to record such index values in a way they are actually out of order, then the best solution is to add a sort():



           var imagesOrder = [
          { index: 2, url: '/three' }, { index: 0, url: '/one' }, { index: 1, url: '/two' }
          ];

          let response = await Users.updateOne(
          { id },
          { "$set": {
          "images": imagesOrder.sort((a,b) => a.index - b.index)
          .map(({ url }) => url)
          }}
          );

          // { "$set": { "images": ["/one","/two","/three"] } }




          As for what you "attempted", it's not really benefiting you in any way to actually attempt updating each element at a given position. But if you really wanted to see it done, then again you actually would just instead build up a single update request:



           var imagesOrder = [
          { index: 2, url: '/three' }, { index: 0, url: '/one' }, { index: 1, url: '/two' }
          ];

          var update = { $set: {} };

          for ( let { url, index } of imagesOrder.sort((a,b) => a.index - b.index) ) {
          update.$set['images.'+index] = url;
          }

          /*
          * Creates:
          *
          * { "$set": {
          * "images.0": "/one",
          * "images.1": "/two",
          * "images.2": "/three"
          * }}
          */

          let response = await Users.updateOne({ id }, update);


          Or in the case where the index property was not there or irrelevant since the array is already ordered:



           var imagesOrder = [
          { index: 2, url: '/three' }, { index: 0, url: '/one' }, { index: 1, url: '/two' }
          ];

          for ( let [index, { url }] of Object.entries(imagesOrder) ) {
          update.$set['images.'+index] = url;
          }

          /*
          * Creates:
          *
          * { "$set": {
          * "images.0": "/one",
          * "images.1": "/two",
          * "images.2": "/three"
          * }}
          */

          let response = await Users.updateOne({ id }, update);


          So it's all pretty much the same thing. Note the common form of notation is actually a "string" for the key which includes the index position numerically. This is described in Dot Notation within the core documentation for the MongoDB query language.



          The one main difference here is that should your new array contain more entries than the actual array stored in the document to be modified, that second form using the "dot notation" to the indexed position is going to fail since it cannot "set" an index position which does not exist.



          For this reason even though there are other pitfalls to "replacing" the array as the original examples show, it's a lot safer than attempting to update via the positional index in the stored document.





          Note that this should be enough to have you at least started in the right direction. Making this work with multiple users possibly updating the data at once can become pretty complicated in terms of update statements for both checking and merging changes.



          In most cases the simple "replace" will be more than adequate at least for a while. And of course the main lesson here should be to not loop "async" methods in places where it is completely unnecessary. Most of the time what you really want to "loop" is the construction of the statement, if of course any looping is required at all and most of the time it really isn't.





          Addendum



          Just in case you or anyone had it in mind to actually store an array of objects with the index position values stored within them, this can become a little more complex, but it can also serve as an example of how to actually issue an update statement which does not "replace" the array and actually is safe considering it does not rely on indexed positions of the array but instead using matching conditions.



          This is possible with the positional filtered $[<identifier>] syntax introduced in MongoDB 3.6. This allows conditions to specify which element to update ( i.e by matching url ) instead of including the index positions within the statement directly. It's safer since if no matching element is found, then the syntax allows for not attempting to change anything at all.



          Also as demonstration the method to $sort the elements based on updated index values is shown. Noting this actually uses the $push modifier even though in this statement we are not actually adding anything to the array. Just reordering the elements. But it's how you actually do that atomically:



          const { Schema, Types: { ObjectId } } = mongoose = require('mongoose');

          const uri = 'mongodb://localhost:27017/longorder';
          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 imageSchema = new Schema({
          index: Number,
          url: String
          })

          const userSchema = new Schema({
          images: [imageSchema]
          });

          const User = mongoose.model('User', userSchema);

          // log helper

          const log = data => console.log(JSON.stringify(data, undefined, 2));

          (async function() {

          try {

          const conn = await mongoose.connect(uri, opts);

          // clean models
          await Promise.all(
          Object.entries(conn.models).map(([k,m]) => m.deleteMany())
          );

          // Create data

          let _id = new ObjectId();

          let user = await User.findOneAndUpdate(
          { _id },
          {
          '$push': {
          'images': {
          '$each': [
          { index: 2, url: '/one' },
          { index: 0, url: '/three' },
          { index: 1, url: '/two' }
          ],
          '$sort': { 'index': 1 }
          }
          }
          },
          { 'new': true, 'upsert': true }
          );

          log(user);

          // Change order request
          let orderImages = [
          { index: 2, url: '/three' },
          { index: 0, url: '/one' },
          { index: 1, url: '/two' }
          ];

          let $set = { };
          let arrayFilters = ;

          for ( { index, url } of orderImages ) {
          let key = url.replace(/^//,'');
          arrayFilters.push({ [`${key}.url`]: url });
          $set[`images.$[${key}].index`] = index;
          }

          let ops = [
          // Update the index value of each matching item
          { 'updateOne': {
          'filter': { _id },
          'update': { $set },
          arrayFilters
          }},
          // Re-sort the array by index value
          { 'updateOne': {
          'filter': { _id },
          'update': {
          '$push': {
          'images': { '$each': , '$sort': { 'index': 1 } }
          }
          }
          }}
          ];

          log(ops);

          let response = await User.bulkWrite(ops);
          log(response);

          let newuser = await User.findOne({ _id });
          log(newuser);


          } catch(e) {
          console.error(e)
          } finally {
          mongoose.disconnect()
          }

          })()


          And the output, showing initial document state, the update and actual changes made:



          Mongoose: users.deleteMany({}, {})
          Mongoose: users.findOneAndUpdate({ _id: ObjectId("5bf6116621293f2ab3dec3d3") }, { '$setOnInsert': { __v: 0 }, '$push': { images: { '$each': [ { _id: ObjectId("5bf6116621293f2ab3dec3d6"), index: 2, url: '/one' }, { _id: ObjectId("5bf6116621293f2ab3dec3d5"), index: 0, url: '/three' }, { _id: ObjectId("5bf6116621293f2ab3dec3d4"), index: 1, url: '/two' } ], '$sort': { index: 1 } } } }, { upsert: true, remove: false, projection: {}, returnOriginal: false })
          {
          "_id": "5bf6116621293f2ab3dec3d3",
          "__v": 0,
          "images": [
          {
          "_id": "5bf6116621293f2ab3dec3d5",
          "index": 0,
          "url": "/three"
          },
          {
          "_id": "5bf6116621293f2ab3dec3d4",
          "index": 1,
          "url": "/two"
          },
          {
          "_id": "5bf6116621293f2ab3dec3d6",
          "index": 2,
          "url": "/one"
          }
          ]
          }
          [
          {
          "updateOne": {
          "filter": {
          "_id": "5bf6116621293f2ab3dec3d3"
          },
          "update": {
          "$set": {
          "images.$[three].index": 2,
          "images.$[one].index": 0,
          "images.$[two].index": 1
          }
          },
          "arrayFilters": [
          {
          "three.url": "/three"
          },
          {
          "one.url": "/one"
          },
          {
          "two.url": "/two"
          }
          ]
          }
          },
          {
          "updateOne": {
          "filter": {
          "_id": "5bf6116621293f2ab3dec3d3"
          },
          "update": {
          "$push": {
          "images": {
          "$each": ,
          "$sort": {
          "index": 1
          }
          }
          }
          }
          }
          }
          ]
          Mongoose: users.bulkWrite([ { updateOne: { filter: { _id: 5bf6116621293f2ab3dec3d3 }, update: { '$set': { 'images.$[three].index': 2, 'images.$[one].index': 0, 'images.$[two].index': 1 } }, arrayFilters: [ { 'three.url': '/three' }, { 'one.url': '/one' }, { 'two.url': '/two' } ] } }, { updateOne: { filter: { _id: 5bf6116621293f2ab3dec3d3 }, update: { '$push': { images: { '$each': , '$sort': { index: 1 } } } } } } ], {})
          {
          "ok": 1,
          "writeErrors": ,
          "writeConcernErrors": ,
          "insertedIds": ,
          "nInserted": 0,
          "nUpserted": 0,
          "nMatched": 2,
          "nModified": 2,
          "nRemoved": 0,
          "upserted": ,
          "lastOp": {
          "ts": "6626503031506599940",
          "t": 139
          }
          }
          Mongoose: users.findOne({ _id: ObjectId("5bf6116621293f2ab3dec3d3") }, { projection: {} })
          {
          "_id": "5bf6116621293f2ab3dec3d3",
          "__v": 0,
          "images": [
          {
          "_id": "5bf6116621293f2ab3dec3d6",
          "index": 0,
          "url": "/one"
          },
          {
          "_id": "5bf6116621293f2ab3dec3d4",
          "index": 1,
          "url": "/two"
          },
          {
          "_id": "5bf6116621293f2ab3dec3d5",
          "index": 2,
          "url": "/three"
          }
          ]
          }





          share|improve this answer














          So that is not actually the way you would do this. Basically there is no need to actually send an update request to the server for every single indexed position for the array. Also the update() method is asynchronous, so it's not something you ever put inside a forEach() which does not respect awaiting the completion of an asynchronous call.



          Instead what is usually the most practical solution is to just $set the entire array content in one request. Also mocking up your imagesOrder to something practical since forEach() even actually has the signature of .forEach((<element>,><index>) => ..., which seems different to what you were expecting given the code in the question.



           var imagesOrder = [
          { index: 0, url: '/one' }, { index: 1, url: '/two' }, { index: 2, url: '/three' }
          ];

          let response = await Users.updateOne(
          { id },
          { "$set": { "images": imagesOrder.map(({ url }) => url) } }
          );

          // { "$set": { "images": ["/one","/two","/three"] } }


          Much like the forEach() a map() does the same array iteration but with the difference that it actually returns an array generated by the processing function. This is actually what you want since all that is needed here is to simply extract the values of the url property from each object.



          Note the index properties are actually already in order and really redundant here, but I'm just approximating what it sounds like you have from your question. Since an "array" actually maintains it's own order then such a property "should" be redundant and it would be advisable that your source array data actually conforms to this.



          If however you managed to record such index values in a way they are actually out of order, then the best solution is to add a sort():



           var imagesOrder = [
          { index: 2, url: '/three' }, { index: 0, url: '/one' }, { index: 1, url: '/two' }
          ];

          let response = await Users.updateOne(
          { id },
          { "$set": {
          "images": imagesOrder.sort((a,b) => a.index - b.index)
          .map(({ url }) => url)
          }}
          );

          // { "$set": { "images": ["/one","/two","/three"] } }




          As for what you "attempted", it's not really benefiting you in any way to actually attempt updating each element at a given position. But if you really wanted to see it done, then again you actually would just instead build up a single update request:



           var imagesOrder = [
          { index: 2, url: '/three' }, { index: 0, url: '/one' }, { index: 1, url: '/two' }
          ];

          var update = { $set: {} };

          for ( let { url, index } of imagesOrder.sort((a,b) => a.index - b.index) ) {
          update.$set['images.'+index] = url;
          }

          /*
          * Creates:
          *
          * { "$set": {
          * "images.0": "/one",
          * "images.1": "/two",
          * "images.2": "/three"
          * }}
          */

          let response = await Users.updateOne({ id }, update);


          Or in the case where the index property was not there or irrelevant since the array is already ordered:



           var imagesOrder = [
          { index: 2, url: '/three' }, { index: 0, url: '/one' }, { index: 1, url: '/two' }
          ];

          for ( let [index, { url }] of Object.entries(imagesOrder) ) {
          update.$set['images.'+index] = url;
          }

          /*
          * Creates:
          *
          * { "$set": {
          * "images.0": "/one",
          * "images.1": "/two",
          * "images.2": "/three"
          * }}
          */

          let response = await Users.updateOne({ id }, update);


          So it's all pretty much the same thing. Note the common form of notation is actually a "string" for the key which includes the index position numerically. This is described in Dot Notation within the core documentation for the MongoDB query language.



          The one main difference here is that should your new array contain more entries than the actual array stored in the document to be modified, that second form using the "dot notation" to the indexed position is going to fail since it cannot "set" an index position which does not exist.



          For this reason even though there are other pitfalls to "replacing" the array as the original examples show, it's a lot safer than attempting to update via the positional index in the stored document.





          Note that this should be enough to have you at least started in the right direction. Making this work with multiple users possibly updating the data at once can become pretty complicated in terms of update statements for both checking and merging changes.



          In most cases the simple "replace" will be more than adequate at least for a while. And of course the main lesson here should be to not loop "async" methods in places where it is completely unnecessary. Most of the time what you really want to "loop" is the construction of the statement, if of course any looping is required at all and most of the time it really isn't.





          Addendum



          Just in case you or anyone had it in mind to actually store an array of objects with the index position values stored within them, this can become a little more complex, but it can also serve as an example of how to actually issue an update statement which does not "replace" the array and actually is safe considering it does not rely on indexed positions of the array but instead using matching conditions.



          This is possible with the positional filtered $[<identifier>] syntax introduced in MongoDB 3.6. This allows conditions to specify which element to update ( i.e by matching url ) instead of including the index positions within the statement directly. It's safer since if no matching element is found, then the syntax allows for not attempting to change anything at all.



          Also as demonstration the method to $sort the elements based on updated index values is shown. Noting this actually uses the $push modifier even though in this statement we are not actually adding anything to the array. Just reordering the elements. But it's how you actually do that atomically:



          const { Schema, Types: { ObjectId } } = mongoose = require('mongoose');

          const uri = 'mongodb://localhost:27017/longorder';
          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 imageSchema = new Schema({
          index: Number,
          url: String
          })

          const userSchema = new Schema({
          images: [imageSchema]
          });

          const User = mongoose.model('User', userSchema);

          // log helper

          const log = data => console.log(JSON.stringify(data, undefined, 2));

          (async function() {

          try {

          const conn = await mongoose.connect(uri, opts);

          // clean models
          await Promise.all(
          Object.entries(conn.models).map(([k,m]) => m.deleteMany())
          );

          // Create data

          let _id = new ObjectId();

          let user = await User.findOneAndUpdate(
          { _id },
          {
          '$push': {
          'images': {
          '$each': [
          { index: 2, url: '/one' },
          { index: 0, url: '/three' },
          { index: 1, url: '/two' }
          ],
          '$sort': { 'index': 1 }
          }
          }
          },
          { 'new': true, 'upsert': true }
          );

          log(user);

          // Change order request
          let orderImages = [
          { index: 2, url: '/three' },
          { index: 0, url: '/one' },
          { index: 1, url: '/two' }
          ];

          let $set = { };
          let arrayFilters = ;

          for ( { index, url } of orderImages ) {
          let key = url.replace(/^//,'');
          arrayFilters.push({ [`${key}.url`]: url });
          $set[`images.$[${key}].index`] = index;
          }

          let ops = [
          // Update the index value of each matching item
          { 'updateOne': {
          'filter': { _id },
          'update': { $set },
          arrayFilters
          }},
          // Re-sort the array by index value
          { 'updateOne': {
          'filter': { _id },
          'update': {
          '$push': {
          'images': { '$each': , '$sort': { 'index': 1 } }
          }
          }
          }}
          ];

          log(ops);

          let response = await User.bulkWrite(ops);
          log(response);

          let newuser = await User.findOne({ _id });
          log(newuser);


          } catch(e) {
          console.error(e)
          } finally {
          mongoose.disconnect()
          }

          })()


          And the output, showing initial document state, the update and actual changes made:



          Mongoose: users.deleteMany({}, {})
          Mongoose: users.findOneAndUpdate({ _id: ObjectId("5bf6116621293f2ab3dec3d3") }, { '$setOnInsert': { __v: 0 }, '$push': { images: { '$each': [ { _id: ObjectId("5bf6116621293f2ab3dec3d6"), index: 2, url: '/one' }, { _id: ObjectId("5bf6116621293f2ab3dec3d5"), index: 0, url: '/three' }, { _id: ObjectId("5bf6116621293f2ab3dec3d4"), index: 1, url: '/two' } ], '$sort': { index: 1 } } } }, { upsert: true, remove: false, projection: {}, returnOriginal: false })
          {
          "_id": "5bf6116621293f2ab3dec3d3",
          "__v": 0,
          "images": [
          {
          "_id": "5bf6116621293f2ab3dec3d5",
          "index": 0,
          "url": "/three"
          },
          {
          "_id": "5bf6116621293f2ab3dec3d4",
          "index": 1,
          "url": "/two"
          },
          {
          "_id": "5bf6116621293f2ab3dec3d6",
          "index": 2,
          "url": "/one"
          }
          ]
          }
          [
          {
          "updateOne": {
          "filter": {
          "_id": "5bf6116621293f2ab3dec3d3"
          },
          "update": {
          "$set": {
          "images.$[three].index": 2,
          "images.$[one].index": 0,
          "images.$[two].index": 1
          }
          },
          "arrayFilters": [
          {
          "three.url": "/three"
          },
          {
          "one.url": "/one"
          },
          {
          "two.url": "/two"
          }
          ]
          }
          },
          {
          "updateOne": {
          "filter": {
          "_id": "5bf6116621293f2ab3dec3d3"
          },
          "update": {
          "$push": {
          "images": {
          "$each": ,
          "$sort": {
          "index": 1
          }
          }
          }
          }
          }
          }
          ]
          Mongoose: users.bulkWrite([ { updateOne: { filter: { _id: 5bf6116621293f2ab3dec3d3 }, update: { '$set': { 'images.$[three].index': 2, 'images.$[one].index': 0, 'images.$[two].index': 1 } }, arrayFilters: [ { 'three.url': '/three' }, { 'one.url': '/one' }, { 'two.url': '/two' } ] } }, { updateOne: { filter: { _id: 5bf6116621293f2ab3dec3d3 }, update: { '$push': { images: { '$each': , '$sort': { index: 1 } } } } } } ], {})
          {
          "ok": 1,
          "writeErrors": ,
          "writeConcernErrors": ,
          "insertedIds": ,
          "nInserted": 0,
          "nUpserted": 0,
          "nMatched": 2,
          "nModified": 2,
          "nRemoved": 0,
          "upserted": ,
          "lastOp": {
          "ts": "6626503031506599940",
          "t": 139
          }
          }
          Mongoose: users.findOne({ _id: ObjectId("5bf6116621293f2ab3dec3d3") }, { projection: {} })
          {
          "_id": "5bf6116621293f2ab3dec3d3",
          "__v": 0,
          "images": [
          {
          "_id": "5bf6116621293f2ab3dec3d6",
          "index": 0,
          "url": "/one"
          },
          {
          "_id": "5bf6116621293f2ab3dec3d4",
          "index": 1,
          "url": "/two"
          },
          {
          "_id": "5bf6116621293f2ab3dec3d5",
          "index": 2,
          "url": "/three"
          }
          ]
          }






          share|improve this answer














          share|improve this answer



          share|improve this answer








          edited Nov 22 at 2:17

























          answered Nov 22 at 1:21









          Neil Lunn

          1




          1












          • Thank you for this great explanation. This is super helpful!
            – Kris
            2 days ago


















          • Thank you for this great explanation. This is super helpful!
            – Kris
            2 days ago
















          Thank you for this great explanation. This is super helpful!
          – Kris
          2 days ago




          Thank you for this great explanation. This is super helpful!
          – Kris
          2 days ago


















           

          draft saved


          draft discarded



















































           


          draft saved


          draft discarded














          StackExchange.ready(
          function () {
          StackExchange.openid.initPostLogin('.new-post-login', 'https%3a%2f%2fstackoverflow.com%2fquestions%2f53422086%2fmongodb-change-the-order-of-an-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

          What visual should I use to simply compare current year value vs last year in Power BI desktop

          How to ignore python UserWarning in pytest?

          Alexandru Averescu