1

I'm trying to find my way around MongoDB. It's the first time I'm using this database, coming from MySQL. But for a chat application I'm making, MongoDB was recommended as a better fit.

I have two collections:

conversations in which I store the members userID (which is stored in a MySQL database) and the join date.

{
    "_id" : ObjectId("5e35f2c840713a43aeeeb3d9"),
    "members" : [ 
        {
            "uID" : "1",
            "j" : 1580580922
        }, 
        {
            "uID" : "4",
            "j" : 1580580922
        }, 
        {
            "uID" : "5",
            "j" : 1580580922
        }
    ]
}

messages in which I store the sender (userID), message, timestamp, conversationID (from the collection above), read and delivered status

{
    "_id" : ObjectId("5e35ee5f40713a43aeeeb1c5"),
    "c_ID" : ObjectId("5e35f2c840713a43aeeeb3d9"),
    "fromID" : "1",
    "msg" : "What's up?",
    "t" : 1580591922,
    "d" : {
        "4" : 1580592039
    },
    "r" : {
        "4" : 1580592339
    }
}

What I want to do now is query the conversations for a specific user, let's say userID 1, together with the last message sent in that conversation.

I came up with the following:

db.getCollection('conversations').aggregate(
[{
    $match: {
        "members.uID": "1"
    }
},
{
    $lookup: {
        as: 'lastMessage',
        foreignField: 'c_ID',
        from: 'messages',
        localField: '_id',
    }
},
])

But the problem here is that it lists all the messages, not only the last one. So I would like to limit this to 1, or if there is an alternative approach.. please let me know.

Any help is appreciated!

6
  • If you're not tied to these particular schemas, I would recommend storing the messages as an array in the conversation. That should greatly simplify any aggregation query you wanted to perform to find particular message(s) within a conversation (whether that's first message, most recent message, all messages from a certain user, etc). This will also probably be a better fit for how your chat application would use conversations and messages. Essentially, this is eliminating the "join" that is needed to find messages in a conversation. Commented Feb 5, 2020 at 14:22
  • I did consider that, but then I would need to perform an update every time a message is added. That's not very performant, is it? Commented Feb 5, 2020 at 15:29
  • An insert will likely be faster than an update, but probably not by much if you're updating the conversation by _id and only adding new messages. I think the benefits of having the messages contained within the conversation outweigh the slightly slower write performance: Your data is more naturally structured, making it easier conceptually to work with, and read performance is improved, as now you no longer need two calls to the DB for the application to load a conversation (1 to get the conversation, then 1 to get all the messages in the conversation). Commented Feb 5, 2020 at 16:45
  • Would the solution below be bad? Also, if there are a lot of messages, holding all the IDs could reach the 16mb limit, no? Commented Feb 5, 2020 at 16:55
  • There's nothing wrong with the solution below that I see (although it's been a while since I did much with aggregation). You could run into the document size limit with lots of messages, so if that's a realistic concern I'd consider keeping a separate collection named oldMessages: Each object in this collection contains an array of messages, and optionally an _id of another object in oldMessages for even older messages. Your conversation entries may or may not have an "oldMessages" field, which if present is the _id of the next oldest set of messages after the ones already there. Commented Feb 5, 2020 at 18:05

1 Answer 1

1

I guess we can understand the last message from timestamp field.

After $match, and $lookup stages, we need to $unwind messages, and then $sort by timestamp.

Now the first message in the messages array is the lastMessage, so when we group, we push the first message as lastMessage, and finally $replaceRoot to shape our result.

If so you can use the following aggregation:

db.conversations.aggregate([
  {
    $match: {
      "members.uID": "1"
    }
  },
  {
    $lookup: {
      foreignField: "c_ID",
      from: "messages",
      localField: "_id",
      as: "messages"
    }
  },
  {
    "$unwind": "$messages"
  },
  {
    "$sort": {
      "messages.t": -1
    }
  },
  {
    "$group": {
      "_id": "$_id",
      "lastMessage": {
        "$first": "$messages"
      },
      "allFields": {
        "$first": "$$ROOT"
      }
    }
  },
  {
    "$replaceRoot": {
      "newRoot": {
        "$mergeObjects": [
          "$allFields",
          {
            "lastMessage": "$lastMessage"
          }
        ]
      }
    }
  },
  {
    $project: {
      messages: 0
    }
  }
])

If the messages array is already sorted, the solution can be simplified, but this is a general solution.

Playground

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

3 Comments

Seems to do what I want! Only thing I have left is, I now have messages and lastMessage, containing the same data. Can I "hide" or remove the messages object? Or is it needed to perform the query?
@PennyWise you can easily remove unwanted fields using project stage, I updated the answer. Please don't forget to mark this answer and upvote.
I seem to miss the project stage in the updated answer. Can you add it again?

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.