Jeff Locke
Jeff Locke

Reputation: 627

Rails scope / class method to select items where association is present

Can't seem to wrap my head around this problem. I have a message model below

Message
 # content:string
 # original_id:integer
 # sender_id:integer
 # receiver_id:integer

 has_one :reply, class_name: "Message", foreign_key: "original_id"
 belongs_to :original, class_name: "Message"

Each message can only have one reply and the reply message will have its corresponding original message.

What I'd like to do is create a scope or a class method that allows me to pull replied messages in one batch and unreplied messages in another.

Something like

 # return messages that have a reply present
 def self.replied
   where(reply.present?)
 end

 # return messages that have no reply
 def self.unreplied
   where(reply.nil?)
 end

so I can chain the methods and ultimately pull messages with

 user1.messages.replied

It doesn't currently work because I can't use the where clause unless it's a DB column...so I was thinking about adding a "replied" boolean column into the DB so I could use the where clause but there's probably a solution to this that I'm just not thinking about. A scope with a lambda? I'm stuck right now.

Any help much appreciated

Upvotes: 2

Views: 2152

Answers (1)

PinnyM
PinnyM

Reputation: 35533

To find those that have been replied is fairly straightforward:

scope :replied, joins(:reply)

as anything without a reply will be filtered out with an INNER JOIN. To find those without replies is a bit more complex - you can either use a LEFT JOIN or an EXISTS subquery to accomplish this. includes is a simple way to force a LEFT JOIN:

scope :unreplied, includes(:reply).
                  where(replies_messages: {id: nil}).
                  where(original_id: nil)

An EXISTS subquery may be somewhat more efficient, but more complex to write (at this time), as it would involve invoking Arel tables (or Squeel). For most cases a LEFT JOIN would be 'good enough', and includes is a quick-and-dirty way to force the API to use one.

Upvotes: 6

Related Questions