Search code examples
javascriptnode.jsmongodbmongoose

Mongoose not populating related documents in another collection


I'm messing about with a project - to track rewards for my kids - to learn Node and MongoDB/Mongoose and hitting an issue (it might be related to trying to get to grips with NoSQL with a background in SQL DBs).

I have two Schemas (that are applicable), Child and RewardDeposit.

Child

models.mongoose.Child = mongoose.model('Child', new mongoose.Schema({
  firstname: {
    type: String,
    required: true,
  },
  lastname: {
    type: String,
    required: true,
  },
  nickname: String,
  datecreated: {
    type: Date,
    default: () => Date.now(),
    immutable: true,
  },
  datemodified: Date,
  deposits: [
    {
      type: mongoose.Schema.Types.ObjectId,
      ref: 'RewardDeposit'
    }
  ]
}));

RewardDeposit

models.mongoose.RewardDeposit = mongoose.model('RewardDeposit', new mongoose.Schema({
  for: String,
  qty: Number,
  datecreated: {
    type: Date,
    default: () => Date.now(),
    immutable: true,
  },
  datemodified: Date,
  child: {type: mongoose.Types.ObjectId, ref: "Child"}
}));

I run a test to create a Child, then create a RewardDeposit related to the child

var cid = "";

const c = new models.mongoose.Child;
c.firstname = "XXXXX";
c.lastname = "XXXXXX";
c.nickname = "XXXXXXX";
c.save()
.then((c) => {
  cid = c._id;
  const d = new models.mongoose.RewardDeposit;
  d.for = "YYYY";
  d.qty = 10;
  d.child = c;
  d.save()
})

No matter which way I write my populate/populated commands, the deposits array in the Child document is always empty

models.mongoose.Child.findById(cid).populate('deposits');

Do I have to have the RewardDeposits as a SubDocument? Can related top-level documents not be populated? Have I done my Schema wrong?

Child object

{
    "_id": "6707fd5f98df9d7355b35580",
    "deposits": [],
    "datecreated": "2024-10-10T16:14:23.719Z",
    "firstname": "XXXXX",
    "lastname": "XXXXXX",
    "nickname": "XXXXXXX",
    "__v": 0
}

Reward Object

{
    "_id": "6707fd6098df9d7355b35583",
    "datecreated": "2024-10-10T16:14:24.630Z",
    "for": "YYYY",
    "qty": 10,
    "child": "6707fd5f98df9d7355b35580",
    "__v": 0
}

Solution

  • You created new RewardDeposit and Child but didn't update the Child with RewardDiposit's id. I think thats why the deposits is empty on Child object but you have reward object cuz you .save() it and forgot to update the Child.

    Took me a while to read your code cuz I haven't seen code written in nodejs like this.

    But I think this code that I have written will work or give you some idea on how it might work.

    var cid = "";
    
    const c = new models.mongoose.Child;
    c.firstname = "XXXXX";
    c.lastname = "XXXXXX";
    c.nickname = "XXXXXXX";
    c.save() //you created the child with "deposits": []
      .then((savedChild) => {
        cid = savedChild._id;
        const d = new models.mongoose.RewardDeposit;
        d.for = "YYYY";
        d.qty = 10;
        d.child = savedChild; //your child filed is object so I would
                              //suggest you to write savedChild._id
        return d.save(); //you created the  reward deposit with child's id (line above this one)
      })
      //you forgot to update the child's deposits with the new deposit created above
      .then((savedDeposit) => {
        //now update the child's deposits array with the newly created RewardDeposit _id
        return models.mongoose.Child.findByIdAndUpdate(
          savedChild._id, 
          { $push: { deposits: savedDeposit._id } }, 
          { new: true }
        );
      })
      .then((updatedChild) => {
        //check if the deposits in child is updated
        console.log('Updated Child:', updatedChild);
      })
      .catch(err => {
        console.error(err);
      });

    I would like to suggest you to use async/await instead of Promise in this case, cuz the code is hard to read and understand.

    Hope this helps you.

    Edit:

    If you delete a document from RewardDeposit, it will not automatically remove the reference from the deposits of Child document. Mongoose doesnt provide cascading deletes like in some SQL databases.

    1. Remove manually.

    const deleteRewardDeposit = async (depositId) => {
      const deposit = await models.mongoose.RewardDeposit.findById(depositId);
      
      if (deposit) {
        //remove deposit reference from Child's deposits
        await models.mongoose.Child.findByIdAndUpdate(deposit.child, {
          $pull: { deposits: depositId }
        });
    
        //delete the reward deposit
        await models.mongoose.RewardDeposit.findByIdAndDelete(depositId);
      }
    };

    If there are a lot of deposits in a child, I would suggest you index the deposits id in Child model.If you dont know what am I talking about, search for Mongoose Indexes or Mongoose Indexes.

    Also, if you think there will be a lot of deposits in a child. You'll have to make a new model for deposits since, mongodb can only hold upto 16 mb of data per document.

    2. Use Mongoose middleware (Post-Hook).

    You can set up a post-hook on the RewardDeposit schema to automatically remove the reference from the Child whenever RewardDeposit is removed.

    You'll have to write this following code at the bottom of you RewardDeposit model file, and above the line mongoose.model("RewardDeposit", rewardDepositSchema).

    models.mongoose.RewardDeposit.schema.post('findOneAndDelete', async function(doc) {
      if (doc) {
        await models.mongoose.Child.findByIdAndUpdate(doc.child, {
          $pull: { deposits: doc._id }
        });
      }
    });