views:

408

answers:

1

I'm trying to connect the values of two join tables that I have and show the results based on a conditional relationship...and i'm having some problems

I have a Users Model(:name, :password, :email), and Events model(:name, :etc) and Interests model (:name)

I created about 5 records in each model.

Then I created two join tables -> UsersInterests and EventsInterests; each not containing a primary key and only comprised of the user_id/interest_id and event_id/interest_id respectively.

Then I added to the model files the HABTM Relationship

users => has_and_belongs_to_many :interests
events => has_and_belongs_to_many :interests
interests => has_and_belongs_to_many :users
         has_and_belongs_to_many :events

Now I wanted to create a controller that finds only the events where the users interests correspond with the events interests

From working on this for a while I've figured that I need something in the area of

@Events = Event.User.find([condition])
[condition] = where users.interest == event.interest

or something like that... I'm kind of lost..How do you state the find condition?...I know how to do the inner join in sql but I'm looking for the elegant Rails way to do this... any tips guys?

+3  A: 

The elegant ruby way to do this is with named scopes. However because you've decided to use has_and_belongs_to_many relationships instead of has_many :through relationships, you're going to need to define the join with raw SQL, which isn't very elegant. And because of the way Rails handles SQL generation, you will have to make a scope for use with a single user, and a second named scope for use with many users.

Class Event < ActiveRecord::Base
  ...

  #find events that share an interest with a single user
  named_scope :shares_interest_with_user, lambda {|user|
    { :joins => "LEFT JOIN events_interests ei ON ei.event_id = events.id " +
         "LEFT JOIN users_intersets ui ON ui.interest_id = ei.interest_id",
      :conditions => ["ui.user_id = ?", user], :group_by => "events.id"
    }

  #find events that share an interest with a list of users
  named_scope :shares_interest_with_users, lambda {|users|
    { :joins => "LEFT JOIN events_interests ei ON ei.event_id = events.id " +
         "LEFT JOIN users_intersets ui ON ui.interest_id = ei.interest_id",
      :conditions => ["ui.user_id IN ?", users], :group_by => "events.id"
    }    
  }

  #find events that share an interest with any user
  named_scope :shares_interest_with_any_user, lambda {
    { :joins => "LEFT JOIN events_interests ei ON ei.event_id = events.id " +
         "JOIN users_intersets ui ON ui.interest_id = ei.interest_id",
      :conditions => "ui.user_id IS NOT NULL", :group_by => "events.id"
    }    
  }

end

Now you can do this to get all the events a user might be interested in:

@events = Event.shares_interest_with_user(@user)

Or this to get all the events a list of users might be interested in:

@events = Event.shares_interest_with_users(@users)

But as I warned, that's not really elegant.

You can greatly simplify the joins if you redefine your relationships to be has_many through relationships with proper join models instead of HABTM relationships. Your case would require the nested has many through plugin for this to work. N.B. You'll have to add corresponding has_many/belongs_to statements in all of the other models. Even the join models.

Class Event < ActiveRecord::Base
  has_many :event_interests
  has_many :interests, :through => :event_interests
  has_many :user_interests, :through => :interests
  has_many :users, :through => :user_interests
  ...   

  #find events that share an interest with a list of users
  named_scope :shares_interest_with_users, lambda {|user|
    { :joins => :user_interests, :group_by => "events.id",
      :conditions => {:user_interests => {:user_id => user}}
    }    
  }

  #find events that share an interest with any user
  named_scope :shares_interest_with_any_user, lambda {
    { :joins => :user_interests, :group_by => "events.id",
      :conditions => "user_interests.user_id IS NOT NULL"
    }

end

Now, the following will work.

@user = User.first; @users = User.find(1,2,3)

# @events = all events a single user would be interested in
@events = Event.shares_interest_with_users(@user)

# @events = all events any of the listed users would be interested in.
@events = Event.shares_interest_with_users(@user)

You could even define a named scope to select events that haven't happened yet and chain the two:

named_scope :future_events, lambda {
   { :conditions => ["start_time > ?", Time.now]}
}

Events.future_events #=> Events that haven't started yet.

# Events that a user would be interested in but only choose those 
# that haven't started yet.
Events.future_events.shares_interest_with_user(@user) 
EmFi
The question was a little ambiguous. So if I've misinterpreted your question, let me know and I'll update the solution. Regardless of whether I understood the question correctly or not, named scopes are probably the best way to solve your problem.
EmFi
That really seems like alot of what I'm trying to do...I was looking for something in the area of @events = Event.find(:all, :conditions => { :users_interests == :events_interests })but I get the error => "odd number list for Hash"??
ChrisWesAllen
`{:user_interests == :events_interests}` makes absolutely no sense not even in SQL. First of all it's not a valid hash. Second of all :users_interests and :events_interests are both invalid hash keys when given as find conditions unless you explicitly join those tables.
EmFi
ChrisWesAllen
Class names are CamelCase. eg: ClassName. Table names are underscored. eg: class_name. Use the table name versions in has_many/belongs_to definitions.
EmFi
The User.rb => { has_many :users_interests;has_many :interests,:through => :users_interestshas_many :events_interests, :through => :interestshas_many :events, :through => :events_interests }Event.rb=> {has_many :events_interests has_many :interests, :through => :events_interests has_many :users_interests, :through => :interests has_many :users, :through => :user_interests }UsersInterests => { belongs_to :users ; belongs_to :interests }EventsInterets => { belongs_to :interests; belongs_to :events }Can you see where I made any mistakes?
ChrisWesAllen
There were not any adjustments to the table names or class names... what would cause the error of "uninitialized constant Event::EventsInterest"
ChrisWesAllen
What are the table names associated with your EventsInterest/UsersInterest models. Rails will expect events_interests and users_interests. Model name is singular, table name is plural. Other than that you've mispelled EventsInterests in your your comment Also is your join model named EventInterest or EventInterest? Rails won't know what your talking about unless they match the association. EventInterest(has_many :event_interests) EventsInteres(has_many :events_interests)
EmFi