The query lends itself to a joins
rather than an includes
. You can do it, sort of, with a single query:
@conversations = current_user.conversations
.select('conversations.*, max(conversation_messages.created_at) as recent_message_date')
.left_joins(:conversation_messages).group('conversations.id').order('recent_message_date desc')
This will produce the desired result, but it isn't reliable. For example, if you do:
>> @conversations.count
You'll get an error because ActiveRecord will replace the select
clause with something you didn't intend. Calling size
produces different SQL but also results in an error.
To do it right, you need to use a subquery. You can do this while staying in the Rails environment by using ActiveRecord's little-known #from
method. This allows you to generate a subquery and then operate on that subquery, for example, by ordering it or filtering with a where clause.
>> subquery = current_user.conversations
.select('conversations.*, max(conversation_messages.created_at) as recent_message_date')
.left_joins(:conversation_messages)
.group('conversations.id')
The subquery
is now an ActiveRecord association that contains the conversation
objects you want, each having a tentative recent_message_date
attribute. I say tentative because (as shown before) the attribute exists only if ActiveRecord doesn't decide to mess with our select
clause.
To set things in stone, we have to give the subquery a name (you'll want to use the name of the table to avoid problems), and then grab everything from the subquery. Then we can count the resulting records, and we can also reliably sort by or filter from that attribute:
>> @conversations = Conversation.from(subquery, :conversations)
.order('recent_message_date desc')
To make things tidy, we can create a method or two:
class Conversation < ApplicationRecord
def self.with_recent_message_date
select('conversations.*, max(conversation_messages.created_at) as recent_message_date')
.left_joins(:conversation_messages)
.group('conversations.id')
end
def self.most_recent_first
order('recent_message_date desc nulls last')
end
end
Or, if you prefer, you can write the methods as scopes on your class; the result is the same. Now you can write clear, expressive queries:
>> subquery = current_user.conversations.with_recent_message_date
>> @conversations = Conversation.from(subquery, :conversations).most_recent_first
In addition, you can add a filter or whatever else using the new attribute:
>> @conversations.where('recent_message_date > ?', Time.current - 3.days)
and you should be able to reliably paginate this result. I've tried a similar query using will_paginate
and it works as expected.