Rails has_many :through Find by Extra Attributes in Join Model
Asked Answered
L

4

73

New to both Ruby and Rails but I'm book educated by now (which apparently means nothing, haha).

I've got two models, Event and User joined through a table EventUser

class User < ActiveRecord::Base
  has_many :event_users
  has_many :events, :through => :event_users
end

class EventUser < ActiveRecord::Base
  belongs_to :event
  belongs_to :user

  #For clarity's sake, EventUser also has a boolean column "active", among others
end

class Event < ActiveRecord::Base
  has_many :event_users
  has_many :users, :through => :event_users
end

This project is a calendar, in which I have to keep track of people signing up and scratching their name out for a given event. I figure the many to many is a good approach, but I can't do something like this:

u = User.find :first
active_events = u.events.find_by_active(true)

Because events don't actually HAVE that extra data, the EventUser model does. And while I could do:

u = User.find :first
active_events = []
u.event_users.find_by_active(true).do |eu|
  active_events << eu.event
end

This seems to be contrary to "the rails way". Can anyone enlighten me, this has been bugging me for a long time tonight (this morning)?

Lawful answered 3/1, 2009 at 10:39 Comment(1)
You could also create a named scope using Gareth's suggestion.Mansard
B
124

How about adding something like this into your User model?

has_many  :active_events, :through => :event_users, 
          :class_name => "Event", 
          :source => :event, 
          :conditions => ['event_users.active = ?',true]

After that you should be able to get active events for a user just by calling:

User.first.active_events
Balkin answered 3/1, 2009 at 11:5 Comment(6)
I've marked you up, but if I don't get a better answer by morning it's yours.Lawful
See my answer below for a more up-to-date methodBalneology
Thanks, Rails 4 way is: -> { where event_users: { active: true } }Swat
But there is no chance that the condition would be defined by a event_users scope? Like scope :active, where(:active => true)? (using Rails 3.2)Hett
@IvanBlack could you explain where that would fit in with has_many through:? Could you include the whole Rails 4 specific equivalent?Catarinacatarrh
@DamienRoche, ->{where...} instead of :conditions. full versionSwat
B
23

Milan Novota has a good solution – but :conditions is now deprecated and the :conditions => ['event_users.active = ?',true] bit just doesn't seem very rails anyways. I prefer something like this:

has_many :event_users
has_many :active_event_users, -> { where active: true }, class_name: 'EventUser'
has_many :active_events, :through => :active_event_users, class_name: 'Event', :source => :event

After that you should still be able to get active events for a user just by calling:

User.first.active_events
Balneology answered 19/11, 2013 at 17:19 Comment(1)
If you're not yet on Rails 4, you can still follow this excellent example, but you may need to use the deprecated :conditions => {active: true}. Worked for me, thanks!Timothytimour
T
12

Even though your u.events isn't explicitly calling the user_events table, that table is still included in the SQL implicitly because of the necessary joins. So, you can still use that table in your find conditions:

u.events.find(:all, :conditions => ["user_events.active = ?", true])

Of course, if you plan to be doing this lookup a lot then sure, give it a separate association as Milan Novota suggests, but there's no requirement for you to do it that way

Token answered 3/1, 2009 at 12:43 Comment(1)
I like to define all the scopes at one place - in the model, even if I use them just once. It keeps my controllers thin and the overall design more consistent.Balkin
E
9

Well, more responsibility is being put in User model than actually needed, and there is no good reason to do so.

We can first define the scope in EventUser model because where it actually belongs, like:

class EventUser < ActiveRecord::Base
  belongs_to :event
  belongs_to :user

  scope :active,   -> { where(active: true)  }
  scope :inactive, -> { where(active: false) } 
end

Now, a user could have both kind of events: active events as well as inactive events, so we can define the relationship in User model as follows:

class User < ActiveRecord::Base
  has_many :active_event_users,   -> { active },   class_name: "EventUser"
  has_many :inactive_event_users, -> { inactive }, class_name: "EventUser"

  has_many :inactive_events, through: :inactive_event_user,
                             class_name: "Event",
                             source: :event
  has_many :active_events,   through: :active_event_users,
                             class_name: "Event",
                             source: :event
end

The beauty in this technique is that the functionality of being an active or an inactive event belongs to EventUser model, and if in future the functionality needs to be modified, it would be modified only in one place: EventUser model, and the changes will be reflected in all other models.

Eggett answered 10/11, 2016 at 10:6 Comment(1)
your solution made my day :)State

© 2022 - 2024 — McMap. All rights reserved.