简体   繁体   中英

Lookup, group and calculate in mongoose based on two different collections

I am having a hard time understanding aggregation. What I am trying to do is to find all records from one collection and then lookup another collection and do some calculation on it, then return response with everything consolidated.

Exam Schema

const examSchema = new mongoose.Schema({
    examName: {
        type: String,
        trim: true,
        required: [true, "Exam name is missing"],
        maxlength: [60, "Max exam name length is 60 characters"],
    },
    duration: {
        type: Number,
        trim: true,
        default: 0,
        min: [0, "Minimum exam duration is zero minutes"],
    },
}, {
    timestamps: true,
});

module.exports = mongoose.model("Exam", examSchema);  

Question Schema

const questionSchema = new mongoose.Schema({
    _refExamId: [{
        type: mongoose.Schema.Types.ObjectId,
        ref: "Exam",
    }],
    title: {
        type: String,
        trim: true,
        required: [true, "Question is missing"],
    },
    positiveMarks: {
        type: Number,
        required: [true, "Marks for the question is missing"],
        default: 0,
    },
}, {
    timestamps: true,
});

module.exports = mongoose.model("Question", questionSchema);  

_refExamId here is an array as there are chances that a single question might be used in multiple exams.

So, what I would like is to query the exams collection and get all the exams and its related questions and the total marks count of each exam.

Desired Output

{
    exam_name: "Demo exam 1",
    total_time: 30,
    total_marks: 50,
    questions: [{ title: "Question 1" }, { title: "Question 2" }]
},
{
    exam_name: "Demo exam 2",
    total_time: 0,
    total_marks: 0,
    questions: []
}
.
.
.
    

What I have done

const exams = await examModel.aggregate([
    {
        $lookup: {
            from: "questions",
            localField: "_id",
            foreignField: "_refExamId",
            as: "questions"
        },
    }
]);

console.log(exams);

Good, you're working towards the right path.

The following pipeline would give you the desired output.

const result = await Exam.aggregate([
  {
    $lookup: {
      from: 'questions',
      localField: '_id',
      foreignField: '_refExamId',
      as: 'questions'
    }
  },
  {
    $project: {
      exam_name: '$examName',
      total_time: '$duration',
      total_marks: {
        $reduce: {
          input: '$questions',
          initialValue: 0,
          in: { $add: ['$$value', '$$this.positiveMarks'] }
        }
      },
      questions: {
        $map: {
          input: '$questions',
          as: 'question',
          in: { title: '$$question.title' }
        }
      }
    }
  }
]);

Here's a full reproduction script using Node.js, MongoDB, and mongoose:

65198206.js

'use strict';
const mongoose = require('mongoose');
const { Schema } = mongoose;


run().catch(console.error);

async function run () {
  await mongoose.connect('mongodb://localhost:27017/test', {
    useNewUrlParser: true,
    useUnifiedTopology: true
  });

  await mongoose.connection.dropDatabase();


  const examSchema = new Schema({
    examName: {
      type: String,
      trim: true,
      required: [true, 'Exam name is missing'],
      maxlength: [60, 'Max exam name length is 60 characters']
    },
    duration: {
      type: Number,
      trim: true,
      default: 0,
      min: [0, 'Minimum exam duration is zero minutes']
    }
  }, {
    timestamps: true
  });

  const Exam = mongoose.model('Exam', examSchema);


  const questionSchema = new Schema({
    _refExamId: [{
      type: mongoose.Schema.Types.ObjectId,
      ref: 'Exam'
    }],
    title: {
      type: String,
      trim: true,
      required: [true, 'Question is missing']
    },
    positiveMarks: {
      type: Number,
      required: [true, 'Marks for the question is missing'],
      default: 0
    }
  }, {
    timestamps: true
  });


  const Question = mongoose.model('Question', questionSchema);



  const exams = await Exam.create([
    { examName: 'A', duration: 70 },
    { examName: 'B', duration: 90 }
  ]);
  await Question.create([
    {
      _refExamId: [
        exams[0]._id, exams[1]._id
      ],
      title: 'Question a',
      positiveMarks: 10
    },
    {
      _refExamId: [
        exams[0]._id, exams[1]._id
      ],
      title: 'Question b',
      positiveMarks: 20
    },
    {
      _refExamId: [
        exams[0]._id, exams[1]._id
      ],
      title: 'Question c',
      positiveMarks: 30
    }
  ]);

  const result = await Exam.aggregate([
    {
      $lookup: {
        from: 'questions',
        localField: '_id',
        foreignField: '_refExamId',
        as: 'questions'
      }
    },
    {
      $project: {
        exam_name: '$examName',
        total_time: '$duration',
        total_marks: {
          $reduce: {
            input: '$questions',
            initialValue: 0,
            in: { $add: ['$$value', '$$this.positiveMarks'] }
          }
        },
        questions: {
          $map: {
            input: '$questions',
            as: 'question',
            in: { title: '$$question.title' }
          }
        }
      }
    }
  ]);

  console.log(JSON.stringify(result, null, 2));
}

Output

$ node 65198206.js
[
  {
    "_id": "5fcf6ea22bd478354c14ad42",
    "exam_name": "A",
    "total_time": 70,
    "total_marks": 60,
    "questions": [
      {
        "title": "Question a"
      },
      {
        "title": "Question b"
      },
      {
        "title": "Question c"
      }
    ]
  },
  {
    "_id": "5fcf6ea22bd478354c14ad43",
    "exam_name": "B",
    "total_time": 90,
    "total_marks": 60,
    "questions": [
      {
        "title": "Question a"
      },
      {
        "title": "Question b"
      },
      {
        "title": "Question c"
      }
    ]
  }
]

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM