Search code examples
mongodbmongoosemongoose-schema

Mongoose: consistently save document order


Suppose we have a schema like this:

const PageSchema = new mongoose.Schema({
  content: String
  order: Number
})

We want order to be always a unique number between 0 and n-1, where n is the total number of documents.

How can we ensure this when documents are inserted or deleted?

For inserts I currently use this hook:

PageSchema.pre('save', async function () {
  if (!this.order) {
    const lastPage = await this.constructor.findOne().sort({ order: -1 })
    this.order = lastPage ? lastPage.order + 1 : 0
  }
})

This seems to work when new documents are inserted. When documents are removed, I would have to decrease the order of documents of higher order. However, I am not sure which hooks are called when documents are removed.

Efficiency is not an issue for me: there are not many inserts and deletes. It would be totally ok if I could somehow just provide one function, say fix_order, that iterates over the whole collection. How can I install this function such that it gets called whenever documents are inserted or deleted?


Solution

  • You can use findOneAndDelete pre and post hooks to accomplish this.

    As you see in the pre findOneAndDelete hook, we save a reference to the deleted document, and pass it to the postfindOneAndDelete, so that we can access the model using constructor, and use the updateMany method to be able to adjust orders.

    PageSchema.pre("findOneAndDelete", async function(next) {
      this.page = await this.findOne();
      next();
    });
    
    PageSchema.post("findOneAndDelete", async function(doc, next) {
      console.log(doc);
    
      const result = await this.page.constructor.updateMany(
        { order: { $gt: doc.order } },
        {
          $inc: {
            order: -1
          }
        }
      );
    
      console.log(result);
    
      next();
    });
    

    Let's say you have these 3 documents:

    [
        {
            "_id": ObjectId("5e830a6d0dec1443e82ad281"),
            "content": "content1",
            "order": 0,
            "__v": 0
        },
        {
            "_id": ObjectId("5e830a6d0dec1443e82ad282"),
            "content": "content2",
            "order": 1,
            "__v": 0
        },
        {
            "_id": ObjectId("5e830a6d0dec1443e82ad283"),
            "content": "content3",
            "order": 2,
            "__v": 0
        }
    ]
    

    When you delete the content2 with "_id": ObjectId("5e830a6d0dec1443e82ad282") with findOneAndDelete method like this:

    router.delete("/pages/:id", async (req, res) => {
      const result = await Page.findOneAndDelete({ _id: req.params.id });
      res.send(result);
    });
    

    The middlewares will run, and adjust the orders, the remaining 2 documents will look like this:

    [
        {
            "_id": ObjectId("5e830a6d0dec1443e82ad281"),
            "content": "content1",
            "order": 0,
            "__v": 0
        },
        {
            "_id": ObjectId("5e830a6d0dec1443e82ad283"),
            "content": "content3",
            "order": 1,    => DECREASED FROM 2 to 1
            "__v": 0
        }
    ]
    

    Also you had better to include next in your pre save middleware so that other middlewares also work if you add later.

    PageSchema.pre("save", async function(next) {
      if (!this.order) {
        const lastPage = await this.constructor.findOne().sort({ order: -1 });
        this.order = lastPage ? lastPage.order + 1 : 0;
      }
      next();
    });