简体   繁体   中英

How to get array of objects from current collection and data from another collection in same query

I have a users collection. Each user can have multiple friends whose _id values are stored in an array in their user document.

I want to render a list which has a user's list of friend names and under each individual name I want to have a location but the locations are in another document because each user can have multiple locations with no limit on the number, like their visited/favourite places.

These are the 2 queries in isolation but I need to somehow combine them.

This is the query which looks at the user ID values in the user's array of friends and then gets the relevant user info based on the ID.

friends = await User.findOne({ _id: userId })
  .populate("friends", "name mobile", null, { sort: { name: 1 } })
  .exec();

Then, to get the places for a particular user:

const place = await Place.find({ creator: userId });

So, I basically want to list the friends in a loop, each with their places under their name like:

Joe Soap
 - London Bridge
 - Eiffel Tower

Bob Builder
 - Marienplatz

The data in mongoDb look like this:

Users:

{
    "_id": {
        "$oid": "5f2d9ec5053d4a754d6790e8"
    },
    "friends": [{
        "$oid": "5f2da51e053e4a754d5790ec"
    }, {
        "$oid": "5f2da52e053d4a754d6790ed"
    }],
    "name": "Bob",
    "email": "bob@gmail.com",
    "created_at": {
        "$date": "2020-08-07T18:34:45.781Z"
    }
}

Places:

{
    "_id": {
        "$oid": "5f3695d79864bd6c7c94e38a"
    },
    "location": {
        "latitude": -12.345678,
        "longitude": 12.345678
    },
    "creator": {
        "$oid": "5f2da51e053e4a754d5790ec"
    },
}

The first join is basically getting data from the array inside the same user collection. The second one is getting data from another collection, places.

UPDATE: almost working

friends = await User.aggregate([
  { $match: { _id: new mongoose.Types.ObjectId(userId) } },
  {
    $lookup: {
      from: "users",
      localField: "friends",
      foreignField: "_id",
      as: "friends_names",
    },
  },
  { $unwind: "$friends_names" },
  {
    $lookup: {
      from: "places",
      localField: "friends",
      foreignField: "creator",
      as: "friends_places",
    },
  },
  { $unwind: "$friends_places" },
  {
    $project: {
      "friends_names.name": 1,
      "friends_places.saved_at": 1,
    },
  },
]);

Data returned:

[
  {
    _id: 5f2d9ec5053d4a754d6790e8,
    friends_names: { name: 'Bob' },
    friends_places: { saved_at: 2020-08-17T13:40:28.334Z }
  },
  {
    _id: 5f2d9ec5053d4a754d6790e8,
    friends_names: { name: 'Natalie' },
    friends_places: { saved_at: 2020-08-17T13:40:28.334Z }
  }
]

Edit Okay, I believe I have reverse engineered your minimal schema correctly:

const mongoose = require("mongoose");
mongoose.connect("mongodb://localhost/test", { useNewUrlParser: true });
mongoose.set("debug", true);
const db = mongoose.connection;
db.on("error", console.error.bind(console, "connection error:"));
db.once("open", async function () {
  await mongoose.connection.db.dropDatabase();
  // we're connected!

  console.log("Connected");

  const userSchema = new mongoose.Schema({
    friends: [{ type: mongoose.Schema.Types.ObjectId, ref: "User" }],
    name: String,
  });

  const placesSchema = new mongoose.Schema({
    latitude: String,
    creator: { type: mongoose.Schema.Types.ObjectId, ref: "User" },
  });

  const User = mongoose.model("User", userSchema);
  const Place = mongoose.model("Place", placesSchema);

  const bob = new User({ name: "Bob", friends: [] });
  await bob.save();
  const natalie = new User({ name: "Natalie", friends: [bob] });
  await natalie.save();
  //const chris = new User({ name: "Chris", friends: [] });
  //await chris.save();
  const john = new User({ name: "John", friends: [natalie, bob] });
  await john.save();

  const place1 = new Place({ latitude: "Place1", creator: bob });
  const place3 = new Place({ latitude: "Place3", creator: bob });
  const place2 = new Place({ latitude: "Place2", creator: natalie });
  await place1.save();
  await place2.save();
  await place3.save();

  await User.find(function (err, users) {
    if (err) return console.error(err);
    console.log(users);
  });

  await Place.find(function (err, places) {
    if (err) return console.error(err);
    //console.log(places);
  });

  var cc = await mongoose
    .model("User")
    .aggregate([
      { $match: { _id: john._id } },
          { $unwind: "$friends" },
      {
        $lookup: {
          from: "places",
          localField: "friends",
          foreignField: "creator",
          as: "friends_places",
        }
      }, { $lookup: {from: 'users', localField: 'friends', foreignField: '_id', as: 'friend_name'} },//, { $include: 'friends' }
      { $unwind: "$friends_places" }, { $unwind: "$friend_name" }//, { $skip: 1}, {$limit: 1}
    ])
    .exec();
  console.log(cc);
});

This is the most relevant part:

        var cc = await mongoose
    .model("User")
    .aggregate([
      { $match: { _id: john._id } },
          { $unwind: "$friends" },
      {
        $lookup: {
          from: "places",
          localField: "friends",
          foreignField: "creator",
          as: "friends_places",
        }
      }, { $lookup: {from: 'users', localField: 'friends', foreignField: '_id', as: 'friend_name'} },//, { $include: 'friends' }
      { $unwind: "$friends_places" }, { $unwind: "$friend_name" }//, { $skip: 1}, {$limit: 1}
    ])
    .exec();

The first unwind: friends 'flattens' the collections initially. So, basically we've got 'userId | friendId' for each user and friend. Then, for each row, we simply look up places created by him ($lookup). Finally, we unwind friends_places because we don't want them to be rendered as [object] in console output. Additionally, there this $match , because we only want to check one user's friends' places. Considering we want to know friend's name as well, we have to do one more join - this is why there's this second $lookup . After that a simple $unwind to get friend's detail and that's it.

Code yields the following:

[ { _id: 5f3aa3a9c6140e3344c78a45,
    friends: 5f3aa3a9c6140e3344c78a44,
    name: 'John',
    __v: 0,
    friends_places:
     { _id: 5f3aa3a9c6140e3344c78a48,
       latitude: 'Place2',
       creator: 5f3aa3a9c6140e3344c78a44,
       __v: 0 },
    friend_name:
     { _id: 5f3aa3a9c6140e3344c78a44,
       friends: [Array],
       name: 'Natalie',
       __v: 0 } },
  { _id: 5f3aa3a9c6140e3344c78a45,
    friends: 5f3aa3a9c6140e3344c78a43,
    name: 'John',
    __v: 0,
    friends_places:
     { _id: 5f3aa3a9c6140e3344c78a46,
       latitude: 'Place1',
       creator: 5f3aa3a9c6140e3344c78a43,
       __v: 0 },
    friend_name:
     { _id: 5f3aa3a9c6140e3344c78a43,
       friends: [],
       name: 'Bob',
       __v: 0 } },
  { _id: 5f3aa3a9c6140e3344c78a45,
    friends: 5f3aa3a9c6140e3344c78a43,
    name: 'John',
    __v: 0,
    friends_places:
     { _id: 5f3aa3a9c6140e3344c78a47,
       latitude: 'Place3',
       creator: 5f3aa3a9c6140e3344c78a43,
       __v: 0 },
    friend_name:
     { _id: 5f3aa3a9c6140e3344c78a43,
       friends: [],
       name: 'Bob',
       __v: 0 } } ]

So, we've got a flat list of John's friends' places.

Important bit : from: places in $lookup is critical, as we have to use MongoDB's name of the collection, not model name 1 .

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