Scope with join on :has_many :through association

Ruby on-RailsRuby on-Rails-3

Ruby on-Rails Problem Overview


class Users < ActiveRecord::Base
  has_many :meetings, :through => :meeting_participations
  has_many :meeting_participations
end

class Meetings < ActiveRecord::Base
  has_many :users, :through => :meeting_participations
  has_many :meeting_participations
end

class MeetingParticipations < ActiveRecord::Base
  belongs_to :user
  belongs_to :meeting

  scope :hidden, where(:hidden => true)
  scope :visible, where(:hidden => false)
end

hidden is an extra boolean column within the m2m association table. Given some Users instance current_user, I want to do

current_user.meetings.visible

which will retrieve a collection of Meetings for which the user is a participant where the hidden column is false. The closest I have gotten is adding the following scope to the Meetings class

scope :visible, joins(:meeting_participations) & MeetingParticipation.visible

The scope does filter the Meetings against the MeetingParticipations table, however there is no join/condition against the MeetingParticipations table related to current_user.

The issue with this is, if current_user and another_user are both participants for some Meetings instance, a Meetings record in the result set will be returned for each participant that has hidden set to false. If current_user has true set for hidden for all Meetings, if another_user is a participant in any of those same Meetings with hidden set to false, those Meetings will appear in the Meetings.visible result set.

Is it possible to have a scope as I've mentioned above which will properly join on the User instance? If not, can someone recommend a solution to this?

Ruby on-Rails Solutions


Solution 1 - Ruby on-Rails

This is my solution for your problem:

class User < ActiveRecord::Base
  has_many :meeting_participations
  has_many :meetings, :through => :meeting_participations do
   def visible
     where("meeting_participations.visible = ?", true)
   end
  end
end

@user.meetings.visible

Solution 2 - Ruby on-Rails

In Rails 4, you can specify the scope originally defined in the child object in the association itself. Short: you don't have to know the internals of the MeetingParticipation model within the User model.

class User < ActiveRecord::Base
  has_many :meeting_participations
  has_many :meetings, :through => :meeting_participations
  has_many :visible_participations, -> { visible }, :class_name => 'MeetingParticipation'
  has_many :visible_meetings, :source => :meeting, :through => :visible_participations
end

class Meeting < ActiveRecord::Base
  has_many :meeting_participations
  has_many :users, :through => :meeting_participations
end

class MeetingParticipation < ActiveRecord::Base
  belongs_to :user
  belongs_to :meeting

  scope :hidden, -> { where(:hidden => true) }
  scope :visible, -> { where(:hidden => false) }
end

This would allow you to do: user1.visible_meetings and user2.visible_meetings with different result sets

Solution 3 - Ruby on-Rails

The clean, associations way to do it is:

has_many :visible_meetings, -> { merge(MeetingParticipations.visible) },
  :source => :meeting, :through => :meeting_participations

To put it in more generic terms: if you have a chained has_many association you can scope the intermediate (through) association via merging the scope. Probably requires Rails 4+.

Otherwise this would have to be done via creating a (probably unwanted) intermediate scoped association as seen in @Paul Pettengill's answer.

Solution 4 - Ruby on-Rails

current_user.meetings.merge(MeetingParticipations.visible)

Solution 5 - Ruby on-Rails

Here's a one liner:

Meeting.joins(:meeting_participations).where(meeting_participation: { hidden: false, user_id: current_user.id })

This is great because you can make a scope out of it, a function out of it, or simply call it anywhere. You can also add any more restrictions you want to the hash.

Solution 6 - Ruby on-Rails

I know this question was answered a while back but I just encountered a similar issue and was looking around for the best way to handle this. The accepted solution is very simple but I think it would be cleaner by moving the scope of the association from Users to Meeting as should below

class Users < ActiveRecord::Base
  has_many :meetings, :through => :meeting_participations
  has_many :meeting_participations
end

class Meetings < ActiveRecord::Base
  has_many :users, :through => :meeting_participations
  has_many :meeting_participations
  scope :hidden, -> { where('meeting_participations.hidden = ?', true) }
  scope :visible, -> { where('meeting_participations.hidden = ?', false) }
end

class MeetingParticipations < ActiveRecord::Base
  belongs_to :user
  belongs_to :meeting

  scope :hidden, where(:hidden => true)
  scope :visible, where(:hidden => false)
end

With this, you are able to call current_user.meetings.hidden

By design, the meeting now dictates what makes it hidden/visible.

Solution 7 - Ruby on-Rails

You could also do:

current_user.meeting_participations.visible.map(&:meeting)

Solution 8 - Ruby on-Rails

It would seem to me that it is not sensible to use a scope on Meeting for your purpose. A meeting itself has no visibility, but the participation has. So I would suggest an extension on the association within User:

class User < ActiveRecord::Base
  has_many :meetings, :through => :meeting_participations do
    def visible
      ids = MeetingParticipation.
        select(:meeting_id).
        where(:user_id => proxy_owner.id, :visible => true).
        map{|p| p.meeting_id}
      proxy_target.where("id IN (?)", ids)
    end
  end
  ...
end

I hope, this helps.

Attributions

All content for this solution is sourced from the original question on Stackoverflow.

The content on this page is licensed under the Attribution-ShareAlike 4.0 International (CC BY-SA 4.0) license.

Content TypeOriginal AuthorOriginal Content on Stackoverflow
QuestiondeefourView Question on Stackoverflow
Solution 1 - Ruby on-RailsAndrei ErdossView Answer on Stackoverflow
Solution 2 - Ruby on-RailsPaul PettengillView Answer on Stackoverflow
Solution 3 - Ruby on-RailsthisismydesignView Answer on Stackoverflow
Solution 4 - Ruby on-RailswotoView Answer on Stackoverflow
Solution 5 - Ruby on-RailsMirror318View Answer on Stackoverflow
Solution 6 - Ruby on-RailsAbass SesayView Answer on Stackoverflow
Solution 7 - Ruby on-RailsdkniffinView Answer on Stackoverflow
Solution 8 - Ruby on-RailsmoritzView Answer on Stackoverflow