HABTM Relationships and the Join Table - ruby-on-rails

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?

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)

Related

pass parameter from controller to models condition

I'm trying to bind a param to a join via a named scope., but I'm getting an error.
What is the correct way to do that?
class Idea < ActiveRecord::Base
#relations
has_many :votes, :inverse_of => :idea
has_one :has_voted, :class_name => 'Vote', :conditions => ['ip = :ip']
# named scopes
scope :with_vote, lambda {|ip| {
:include => [:has_voted],
# like this ??
:conditions => [:has_voted => {:conditions => {:userIp => ip}} ]
}}
end
Idea.with_vote(request.ip).all
I believe I need the condition definition in the model for it to appear in the ON clause of a JOIN, rather then in the WHERE one.
Edit I'm trying to get the following query
select Ideas.*, Votes.* from Ideas
left outer join Votes
on Votes.Idea_id = Idea.id AND Votes.ip = {request.ip}
I do not think you can use incomplete conditions in an association.
If I understand correctly, you need Idea has many votes and votes records the request.ip and idea id.
You want the scope to retrieve all ideas your current request ip voted for.
class Idea
has_many :votes
scope :with_vote_from_ip, lambda {|ip| {
:include => [:votes],
:conditions => ['votes.ip = ?', ip]
}}
end
but if you want all ideas including only votes from current up you need extra conditions on the outer join. I think this is not possible without sql fragment:
class Idea
has_many :votes
scope :with_vote_from_ip, lambda {|ip| {
:joins => 'left outer join Votes on Votes.Idea_id = Idea.id AND Votes.ip = #{ip}'
}}
end
now Idea.with_vote_from_ip(request.ip).all should work.

Accessing values in a has_many :through join table

I've got users who are members of groups through a membership join table, and one of the attributes of that join table is "administrator". I'm trying to do a check inside of a group's member view, looping through each member to see if they are an administrator.
In the view I tried the following:
for user in #group.users
if user.administrator?
...DO STUFF
end
end
I also tried this in the controller:
#administrators = #group.memberships.find(:all, :conditions => ["administrator = 1"])
But no luck. Any thoughts?
UPDATE - per below, put a method into the user model:
def is_administrator_of(group_id)
Membership.find(:first, :conditions => ['user_id = ? AND group_id = ? AND administrator = ?', self[:id], group_id, true])
end
I think this would be a cleaner way to do this
class Group < ActiveRecord::Base
has_many :memberships
has_many :users, :through => :memberships
has_many :admins, :through => :memberships, :source => :user,
:conditions => ['memberships.administrator = ?', true]
end
You now have a group.admins list
for user in #group.admins
...DO STUFF
end
Although I think you could setup associations to accomplish this I think the easiest way to do it would be to add a method to your User model that allows you to check for each user (this way it would fit in the loop you have provided). I don't know if it will drop right it, may take a few quick changes but you could start with something like:
User Model
def is_administrator_of(group_id)
Membership.find(:first, :conditions => ['user_id = ? AND group_id = ?', self[:id], group_id]).administrator == 1
end

Rails Association Question

I have three models: User, RaceWeek, Race.
Current associations:
class User < ActiveRecord::Base
has_many :race_weeks
end
class RaceWeek < ActiveRecord::Base
belongs_to :user
has_many :races
end
class Race < ActiveRecord::Base
belongs_to :race_week
end
So the user_id is a foreign key in RaceWeek and race_week_id is a foreign key in Race.
fastest_time is an attribute of the Race model.
QUESTION: What's the optimal way to retrieve a list of users who have the top X fastest race times?
You can do it like this:
users = User.all(:limit => X, :joins => {:race_weeks => :races}, :order => "reces.fastest_time DESC").uniq
If you have correctly specified has_many :through association, then you could even do it like this:
users = User.all(:limit => X, :joins => :races, :order => "reces.fastest_time DESC").uniq
In this solution, you get what you want with one query, but two joins. And this uniq method is not very good unless you would use small X.
Something like:
races = Race.all(:order => "fastest_time desc", :limit => X, :include => {:race_week => :user})
users = races.map{|race| race.race_week.user}.uniq
Note: didn't test this.
Given your current model the following should work.
race_weeks = RaceWeek.find_by_sql(["SELECT user_id FROM race_weeks JOIN races ON races.race_week_id = race_weeks.id ORDER BY races.fastest_time desc LIMIT ?", X)
users = User.find(race_weeks.collect(&:user_id).uniq)
I know that it requires two look ups but the second lookup should be very fast since you are only looking up X records by their primary key.

Rails, ActiveRecord: how do I get the results of an association plus some condition?

I have two models, user and group. I also have a joining table groups_users.
I have an association in the group model:
has_many :groups_users
has_many :users, :through=> :groups_users
I would like to add pending_users which would be the same as the users association but contain some conditions. I wish to set it up as an association so that all the conditions are handled in the sql call. I know there's a way to have multiple accessors for the same model, even if the name is not related to what the table names actually are. Is it class_name?
Any help would be appreciated, thanks
Use named_scopes, they're your friend
Have you tried using a named_scope on the Group model?
Because everything is actually a proxy until you actually need the data,
you'll end up with a single query anyway if you do this:
class User < ActiveRecord::Base
named_scope :pending, :conditions => { :status => 'pending' }
and then:
a_group.users.pending
Confirmation
I ran the following code with an existing app of mine:
Feature.find(6).comments.published
It results in this query (ignoring the first query to get feature 6):
SELECT *
FROM `comments`
WHERE (`comments`.feature_id = 6)
AND ((`comments`.`status` = 'published') AND (`comments`.feature_id = 6))
ORDER BY created_at
And here's the relevant model code:
class Feature < ActiveRecord::Base
has_many :comments
class Comment < ActiveRecord::Base
belongs_to :feature
named_scope :published, :conditions => { :status => 'published' }
This should be pretty close - more on has_many.
has_many :pending_users,
:through => :groups_users,
:source => :users,
:conditions => {:pending => true}
:pending is probably called something else - however you determine your pending users. As a side note - usually when you see a user/group model the association is called membership.
In the User model:
named_scope :pending, :include => :groups_users, :conditions => ["group_users.pending = ?", true]
That's if you have a bool column named "pending" in the join table group_users.
Edit:
Btw, with this you can do stuff like:
Group.find(id).users.pending(:conditions => ["insert_sql_where_clause", arguments])

Is it possible to make ActiveRecord create objects for rows loaded using the :joins option?

I need to do something like this
class User < ActiveRecord::Base
has_many :abuse_reports
end
class AbuseReport < ActiveRecord::Base
belongs_to :abuser, :class_name => 'User', :foreign_key => 'abuser_id'
belongs_to :game
end
class Game < ActiveRecord::Base
has_many :abuse_reports
end
#top_abusers = User.page(params[:page],
:joins => [
'JOIN abuse_reports ON users.id = abuse_reports.abuser_id',
'JOIN games ON games.id = abuse_reports.game_id'
],
:group => 'users.id',
:select => 'users.*, count(distinct games.id) AS game_count, count(abuse_reports.id) as abuse_report_count',
:order => 'game_count DESC, abuse_report_count DESC'
)
This works, but doesn't create objects for AbuseReports or Games - it just returns a pile of rows. When I reference these objects from my view it loads them again. Is there a way to fix this? Or some way to get this behavior without using :joins?
Firstly, you should really use :include instead of :joins
User.find(:all, :include => { :abuse_reports => [ :game ] }, :order => )
or, in your case, try
User.page(params[:page], :include => { :abuse_reports => [ :game ] })
This will perform the join for you and retrieve the records in one shot.
Now, this may retrieve a given game record for you multiple times (if the same game is tied to a user by multiple reports.) If your game record is large, you can reduce the amount of data exchanged between your app and the RDBMS as follows:
class User < ActiveRecord::Base
has_many :abuse_reports
has_many :abused_games, :through => :abuse_reports
end
...
User.find(:all, :include => [ :abuse_reports, :abused_games ])
Finally, you also want to retrieve the counts and sort accordingly. Check out http://railscasts.com/episodes/23 for how to add counter caches into the actual active records (counter caches simplify the SQL and make the RDBMS' life easier and your queries run faster). After you set up the counter caches, you can finally alter the above to do:
User.find(:all, :include => [ :abuse_reports, :abused_games ], :order => 'users.abused_games_count DESC, users.abuse_reports_count DESC')
This will ultimately retrieve your ActiveRecords in one single, simple SQL statement.
The problem you are having is that you use ActiveRecord in way its not "supposed" to be used. By that I mean that you are writing your own sql, which makes AR give up all of its control to you.
If you want AR to handle everything you should try to use it with less of your own SQL in there. It looks like you want to know which user has the highest amount of AbuseReports. Try something like this:
some_user.abuse_reports.count
to get the count of abuse_reports

Resources