Krzysztof Podmokły
Krzysztof Podmokły

Reputation: 866

MongoDB query multiple models

I have User and Training model. Both hold likes property set to be an array. The idea is that I would like to create some kind of "add-to-favorite" functionality.

User model

likes: [
    {
      type: mongoose.Schema.ObjectId,
      ref: 'Training',
    },
  ],

Training model

likes: [
    {
      type: mongoose.Schema.ObjectId,
      ref: 'Training',
    },
  ],

In my controller I created a function which is responsible for populating user id in Training and vice versa.

exports.favoriteTraining = catchAsync(async (req, res, next) => {
  const user = await User.findById(req.user.id);
  const training = await Training.findById(req.params.id);

  const trainingLikes = training.likes.filter((like) => {
    return like.toString() === req.user.id;
  });

  if (trainingLikes.length > 0) {
    return next(new AppError('Training already liked', 400));
  }

  user.likes.unshift(req.params.id);
  training.likes.unshift(req.user.id);

  await user.save({ validateBeforeSave: false });
  await training.save({ validateBeforeSave: false });

  res.status(201).json({
    status: 'success',
    data: {
      user,
      training,
    },
  });
});

Current solution works, but I was wondering if there is any other way where I do not need to query both databases separately.

Upvotes: 1

Views: 1901

Answers (1)

SuleymanSah
SuleymanSah

Reputation: 17858

You can simplify your user schema, and the logic to favorite a training by removing the likes from user model.

Here are the steps:

1-) Remove the likes from user model

const mongoose = require("mongoose");

const UserSchema = new mongoose.Schema({
  name: String,
});

module.exports = mongoose.model("User", UserSchema);

2-) Now we only need to modify the likes array in the training model.

exports.favoriteTraining = catchAsync(async (req, res, next) => {
  const loggedUserId = req.user.id;

  let training = await Training.findById(req.params.id);

  const alreadyLiked = training.likes.find((like) => like.toString() === loggedUserId) !== undefined;

  if (alreadyLiked) return next(new AppError("Training already liked", 400));

  training.likes.push(loggedUserId);

  training = await training.save({ validateBeforeSave: false });

  res.status(201).json({
    status: "success",
    data: {
      training,
    },
  });
});

As you see we only made 2 db operations here (it was 4 ). Also I advise to use push instead of unshift, unshift modifies the index of all items in array, push only add the new item to the end.

3-) Since we removed the likes from user, we need to find a way to reference the trainings from user.

Let's say we want to find a user by id and get the trainings he/she favorited. We can use mongodb $lookup aggregation to achieve this. (We could also use virtual populate feature of mongoose, but lookup is better.)

exports.getUserAndFavoritedTrainings = catchAsync(async (req, res, next) => {
  const loggedUserId = req.user.id;

  const result = await User.aggregate([
    {
      $match: {
        _id: mongoose.Types.ObjectId(loggedUserId),
      },
    },
    {
      $lookup: {
        from: "trainings", //must be physcial name of the collection
        localField: "_id",
        foreignField: "likes",
        as: "favorites",
      },
    },
    {
      $project: {
        __v: 0,
        "favorites.likes": 0,
        "favorites.__v": 0,
      },
    },
  ]);

  if (result.length > 0) {
    return res.send(result[0]);
  } else {
    return next(new AppError("User not found", 400));
  }
});

TEST:

Let's say we have these 2 users:

{
    "_id" : ObjectId("5ea7fb904c166d2cc42fd862"),
    "name" : "Krzysztof"
},
{
    "_id" : ObjectId("5ea808988c6f2207c8289191"),
    "name" : "SuleymanSah"
}

And these two trainings:

{
    "_id" : ObjectId("5ea7fbc34c166d2cc42fd863"),
    "likes" : [
        ObjectId("5ea7fb904c166d2cc42fd862"), // Swimming favorited by Krzysztof
        ObjectId("5ea808988c6f2207c8289191") // Swimming favorited by SuleymanSah
    ],
    "name" : "Swimming Training",
},
{
    "_id" : ObjectId("5ea8090d6191c60a00fe9d87"),
    "likes" : [
        ObjectId("5ea7fb904c166d2cc42fd862") // Running favorited by Krzysztof
    ],
    "name" : "Running Training"
}

If the logged in user is Krzysztof, the result will be like this:

{
    "_id": "5ea7fb904c166d2cc42fd862",
    "name": "Krzysztof",
    "favorites": [
        {
            "_id": "5ea7fbc34c166d2cc42fd863",
            "name": "Swimming Training"
        },
        {
            "_id": "5ea8090d6191c60a00fe9d87",
            "name": "Running Training"
        }
    ]
}

You can play with this aggregation in this playground

Upvotes: 3

Related Questions