简体   繁体   中英

Rails 5: How to order models by associated model attribute, and make them distinct

I have a scope in rails that is supposed to order a set of conversations by the last chat message created_at attribute. That goes like this:

class Conversation < ApplicationRecord
    has_many :chat_messages, -> { order(created_at: :asc) }, dependent: :nullify do
      def last_from_user
        from_user.order("created_at ASC").last
      end

      def last_delivered_from_user
        from_user.order("delivered_at ASC").last
      end

      def last_from_agent
        from_agent.order("created_at ASC").last
      end
    end

    default_scope { joins(:chat_messages).order("chat_messages.created_at desc").distinct }
end

When, for example, an organization with id 4 gets a new message I expect the order to be:

[4,1,2,3]

However, what I get is:

[1,2,3,4]

if I remove the distinct method like this:

class Conversation < ApplicationRecord
    default_scope { joins(:chat_messages).order("chat_messages.created_at desc") }
end

Conversations are ordered fine, but get duplicated: [4,1,2,3,4]

my chat_message model looks like this:

    class ChatMessage < ApplicationRecord

      # Associations
      # ------------------------------------------------------------

      belongs_to :conversation
    end

I cannot use the 'unique' method because this would make it impossible for the pagination to work appropriately.

However, when a conversation has more than one message the 'distinct' method messes up with the order. this scope returns the conversations in the order they were created and not the order I actually need.

If we remove Rails and just look at the SQL its clearer. What you have is a classic SQL problem of trying to get a list sorted by the max or min based on a join.

You're running a query like this:

SELECT conversations.*
FROM conversations
INNER JOIN chat_messages
  ON chat_messages.conversation_id = conversations.id
ORDER BY chat_messages.created_at desc

And you will get back a row for every pair of conversations and chat_messages . That's why you have duplicates.

You can't use distinct with this.

SELECT conversations.*
FROM conversations
INNER JOIN chat_messages
  ON chat_messages.conversation_id = conversations.id
ORDER BY chat_messages.created_at desc

ERROR 3065 (HY000): Expression #1 of ORDER BY clause is not in SELECT list,
references column 'test.chat_messages.created_at' which is not in SELECT list;
this is incompatible with DISTINCT

If you use select distinct conversations.*, chat_messages.created_at you're back where you started.

I'm not sure what .distinct is doing, but it should probably raise an exception.


Instead, get rid of the duplicates with group by conversation.id . Now that you have a grouped query you can order by max(chat_messages.created_at) desc .

select c.*
from conversations c
join chat_messages cm
  on cm.conversation_id = c.id
group by c.id
order by max(cm.created_at) desc

Translating this into Rails...

Conversations
  .joins(:chat_messages)
  .group(:id)
  .order("max(chat_messages.created_at) desc")

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