9

I have a Mongoose document (Mongoose 5.4.13, mongoDB 4.0.12):

var SkillSchema = new mongoose.Schema({
    skill: { type: String },
    count: { type: Number, default: 0 },
    associatedUsers: [{ type : mongoose.Schema.Types.ObjectId, ref: 'User' }]
});

That I update as follows:

var query = { skill: req.body.skill };
var update = { $addToSet: { associatedUsers: req.params.id } };
            
var options = { upsert: true, new: true, setDefaultsOnInsert: true };

await skillSchema.findOneAndUpdate(query, update, options);

During this update, I would like to also update count to be equal to the length of associatedUsers.

Ideally I want this to happen at the same time as updating the other fields (i.e not in a subsequent update), either via a pre-hook or within findOneAndUpdate.

I've tried using a pre hook after schema definition:

SkillSchema.pre('findOneAndUpdate', async function(){
    console.log("counting associated users");
    this.count = this.associatedUsers.length;
    next();
});

As well as using aggregate in my UPDATE route:

await skillSchema.aggregate([{ $project: { count: { $size: "$associatedUsers" } } } ])

But I can't get either to work.

Does anyone have any suggestions for how I could achieve this?

1
  • Did the answers below help you? Commented Oct 3, 2019 at 14:28

4 Answers 4

6
+50

You could use $set like this in 4.2 which supports aggregation pipeline in update.

The first $set stage calculates a associatedUsers based on the previous and new value. $setUnion to keep the distinct associatedUsers values.

The second $set stage calculates tally based on the associatedUsers calculated in the previous stage.$size to calculate the length of associatedUsers values.

var query = {skill: req.body.skill};
var update = [{ $set: { "associatedUsers":{"$setUnion":[{"$ifNull":["$associatedUsers",[]]}, [req.params.id]] }}}, {$set:{tally:{ $size: "$associatedUsers" }}}];
var options = { upsert: true, new: true, setDefaultsOnInsert: true };
await skillSchema.findOneAndUpdate(query, update, options)

If any argument resolves to a value of null or refers to a field that is missing, $setUnion returns null. So just needed to safeguard our operation with $ifNull

Sign up to request clarification or add additional context in comments.

7 Comments

This looks like an excellent solution. I will test this tonight
in case of upsert, it'll give error arguement to $size must be an array
@sushantmehta Thanks. Updated answer to account for null values in the set operation. That is behavior for set operators not specially related to upsert.
This isn't working for me: var update = [{ $set: { "associatedUsers":{"$setUnion":[{"$ifNull":["$associatedUsers",[] ]}, [req.params.id]] }}}, { $set: { count: { $size: "$associatedUsers" }}}]; Note: I've changed the schema field tally to count. Will update the question to reflect this
Np. Sorry, expressive updates is a new feature in 4.2. It will not work with older version. You probably have to do in multiple queries.
|
2

About tally and associatedUsers.length

// define your schema object
var schemaObj = {
  skill: { type: String },
  associatedUsers: { type: Array }
};

// get the length of users
var lengthOfAsUsers = schemaObj.associatedUsers.length;

// add tally to schema object and set default to the length of users
schemaObj.tally = { type: Number, default: lengthOfAsUsers };

// and pass your schema object to mongoose.Schema
var SkillSchema = new mongoose.Schema(schemaObj);

module.exports = SkillSchema;

EDIT you can update tally subsequently, but recommended solution would be to use this method https://mongoosejs.com/docs/populate.html

const id = "nameSomeId";

SkillSchema.find({ _id: id }).then(resp => {
  const tallyToUpdate = resp.associatedUsers.length;
  SkillSchema.findOneAndUpdate({ _id: id }, { tally: tallyToUpdate }).then(
    resp => {
      console.log(resp);
    }
  );
});

3 Comments

But is this dynamic? I.e will the tally increase as I add associatedUsers?
Well, you can always update tally subsucvently.. I will edit my answer
sure, but this require I make two requests which is not efficient
1

The solution I have will only work on mongodb v 4.2 as it has option to use aggregate in the update and will only need one query as:

skillSchemafindOneAndUpdate(


 {skill:"art"},
   [
     { $set: { 
       associatedUsers:{
         $cond:{
            if: {$gte: [{$indexOfArray: ["$associatedUsers", mongoose.Types.ObjectId(req.params.id)]}, 0]},
              then: "$associatedUsers",
              else: { $cond:{
                if: { $isArray: "$associatedUsers" },
                then: {$concatArrays:["$associatedUsers",[mongoose.Types.ObjectId(req.params.id)]]},
                else: [mongoose.Types.ObjectId(req.params.id)]
           }}
           }
      }}},
      {$set:{
        associatedUsers:"$associatedUsers",
        tally:{$size:"$associatedUsers"},
      }}
   ],
   {upsert:true,new:true}
)

ref: https://docs.mongodb.com/manual/reference/method/db.collection.update/#update-with-aggregation-pipeline

Comments

0

The "Group" field does not appear in the schema. On MongoDB Shell, these codes will work.

However, Mongoose will also give an error because the schema is validated.

Is the "Group" field a dynamic field? I think the problem with the schema will be solved.

var mongoose = require("mongoose");

var SkillSchema = new mongoose.Schema({
    skill: { type: String },
    tally: { type: Number, default: 0 },
    associatedUsers: { type: Array },
    group: { type: Array }
 });

Comments

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.