views:

340

answers:

5

Hi,

I'm writing a facebook style messaging system for a Rails App and I'm having trouble selecting the Messages for the inbox (with will_paginate).

The messages are organized in threads, in the inbox the most recent message of a thread will appear with a link to it's thread. The thread is organized via a parent_id 1-n relationship with itself.

So far I'm using something like this:

class Message < ActiveRecord::Base
  belongs_to :sender, :class_name => 'User', :foreign_key => "sender_id"
  belongs_to :recipient, :class_name => 'User', :foreign_key => "recipient_id"
  has_many :children, :class_name => "Message", :foreign_key => "parent_id"
  belongs_to :thread, :class_name => "Message", :foreign_key => "parent_id"
end

class MessagesController < ApplicationController

  def inbox
    @messages = current_user.received_messages.paginate :page => params[:page], :per_page => 10, :order => "created_at DESC"
  end
end

That gives me all the messages, but for one thread the thread itself and the most recent message will appear (and not only the most recent message). I can also not use the GROUP BY clause, because for the thread itself (the parent so to say) the parent_id = nil of course.

Anyone got an idea on how to solve this in an elegant way? I already thought about adding the parent_id to the parent itself and then group by parent_id, but I'm not sure if that works.

Thanks

A: 

My solution would be to get a list of threads (which I'm assuming could be obtained by messages with no parent id). Then on the Message model, add a method that will find the latest message in the thread and return it. You can then use that method to obtain the latest method in each thread and put in a link to the head of the thread easily.

(Pseudo-)code:

class Message < ActiveRecord::Base
  belongs_to :sender, :class_name => 'User', :foreign_key => "sender_id"
  belongs_to :recipient, :class_name => 'User', :foreign_key => "recipient_id"
  has_many :children, :class_name => "Message", :foreign_key => "parent_id"
  belongs_to :thread, :class_name => "Message", :foreign_key => "parent_id"

  def get_last_message_in_thread()
    last_message = self
    children.each do |c|
       message = c.get_last_message_in_thread()
       last_message = message if message.created_at > last_message.created_at
    end
    return last_message
  end
end

class MessagesController < ApplicationController

  def inbox
    @messages = current_user.received_messages.find_by_parent_id(Null).paginate :page => params[:page], :per_page => 10, :order => "created_at DESC"
  end
end

You could probably do a lot better than having a recursive function to find the last message in the thread, but it's the simplest solution I can think of to demonstrate the idea. I'm also not sure I have the correct syntax for finding unset parent id's in the inbox function, which is why I marked the code as pseudo code :)

workmad3
Also something I've noticed with my solution... you'd need something else to order by the correct created_date. Probably something along the lines of: @messages = current_user.received_messages.find_by_parent_id(Null).sort_by{|m| m.get_last_message_in_thread().created_at }.paginate :page => params[:page], :per_page => 10 although that's pretty ugly for a one-liner.
workmad3
Thanks for your solution. The problem here is that I want the most recent child-messages (10 per page) shown in the inbox. When I get the threads first, I'll have to get ALL threads and then look for the 10 most recent children in ALL threads (because it is possible that the oldest thread contains the youngest child, so to say). That is not efficient. I'm looking for a solution which minimizes the DB queries.
ole_berlin
A: 

The only efficient way would be to have a Thread model and use GROUP BY as you mentioned - Anything else would require iteration over the messages.

read update in comments

Matt
That's what I thought! But when I add the parent_id to the parent itself, I get a really strange behaviour: When I use "GROUP BY parent_id ORDER BY created_at DESC" I don't get the most recent one, but the oldest one. I have to look into the MySQL Doc again. Gonna post my solution here.
ole_berlin
Ahh - I see what you mean. GROUP BY uses the default sort order of your table. You will need to use an inner query to find MAX(created_at) for each thread and perform a join on that (i don't think the rails helpers will help you in this instance).
Matt
Yeah, but using a Subquery won't help my performance, so it is a bad solution in a way. I know from my last projects that people send a lot of PM in Social Networks, the messages table tends to be one of the largest and the Queries for getting these should be fast and handle a lot of data well.
ole_berlin
I tested the query with 100.000 messages in the table and a GROUP BY statement makes it 6 times slower (without any subselect!). That's already a lot. I don't even want to try the whole thing using a subselect. So I guess the way to go is using a seperate table for the message_threads with a column for the most recent message in this thread and a column for the date of the most recent message. That should do the trick and keep the performance acceptable
ole_berlin
A: 

giving the parent itself as parent makes it very easy to create queries that operate on the whole thread, because you can group (or anything similar) by parent_id.

if you handle the parents differently, all your queries have to take care of this too

knittl
A: 

I figured the only good solution is using a second model to store the most recent messages for every thread (because of performance issues when using GROUP BY with a subselect, see my comments). It won't take a lot of space in the DB because we're only storing id's and no text or even blobs.

The RecentMessages Model would look something like this:

create_table :recent_messages do |t|
  t.integer :sender_id
  t.integer :recipient_id
  t.integer :message_id
  t.integer :message_thread_id

  t.timestamps
end

class RecentMessage < ActiveRecord::Base

  belongs_to :message
  belongs_to :message_thread, :class_name => 'Message'
  belongs_to :sender, :class_name => 'User', :foreign_key => "sender_id"
  belongs_to :recipient, :class_name => 'User', :foreign_key => "recipient_id"

end

The main idea is: All the messages are stored in one model (Messages). Whenever a new message is added to a thread (or a thread is created), two things happen (e.g. with a after_save callback):

  • Store the new message in the RecentMessages model (that means sender_id, recipient_id, message_id, message_thread_id (= parent_id || id))
  • Get the most recent message (from this thread in messages), where sender_id == recipient_id and vice versa (note: This only works if the message model should only support messages between 2 users) and store it in the RecentMessages model as well (if found and if it's not already there)

Of course there should only be max. 2 recent_messages stored in the DB for every message_thread at any given time.

If one wants to show i.e. the inbox, the following has to happen:

@messages = current_user.recent_received_messages.paginate :page => params[:page], :per_page => 10, :order => "created_at DESC", :include => :message

That's the best I figured out so far. I still think it's ugly but it's fast and it works. If anyone comes up with a better solution, I'll be gratefull!

ole_berlin
A: 

I don't know how to accomplish this in Rails, but this is how I did it directly in MySQL:

select * from messages where message_id in ( select max(message_id) from messages where to_uid = 51 group by thread_id ) order by timestamp desc

I used a subquery to grab the most recent message in a thread and then the main query to grab all of the fields for the messages found in the subquery.

SeiNyce