How to create a finder condition that operates on the associated model - ruby-on-rails

Assuming rails app has the following models song, play, radio_station:
song has_many plays
play has_many radio_stations
radio_station has attribute "call_sign"
I want to create one find for Song that would return for me the all songs where the last play (of the song) was played by a radio station of "WKRP"
I could do
found = []
Song.find(:all,:include=>[{:plays=>[:radio_station]}]).each do |song|
found << song if song.plays.last.radio_station.call_sign == "WKRP"
end
but this would mean that all songs must be pulled down from the DB first and looped through... which would get slow as the # of songs and plays builds.
How can I put this into one find condition?
It seems like this is something that should be doable - but I can't figure out how and I am not really an SQL guru ...
and then.. would love to squeeze it into a named scope so i could call it like:
Song.find_all_last_played_at("WKRP")

This probably can't be done by ActiveRecord finders. You have to roll some custom SQL with a subquery (MySQL 5 required):
SELECT s.* FROM songs s LEFT JOIN plays p
WHERE p.radio_station_id = [id of WKRP]
AND p.created_at = (SELECT MAX(created_at) FROM plays p2
WHERE p2.song_id = s.id)
Load these songs by using find_by_sql:
Song.find_by_sql('SELECT ...')
You could refactor it using a named_scope if you want to.
class Song < ActiveRecord::Base
has_many :plays
named_scope :last_played_on, lambda { |radio_station|
{ :conditions => ['plays.created_at = (SELECT MAX(p2.created_at) FROM plays p2
WHERE p2.song_id = songs.id) AND plays.radio_station_id = ?', radio_station.id],
:joins => [:plays] }
}
end
#wkrp = RadioStation.find_by_name('WKRP')
Song.last_played_on(#wkrp).each { |song| ... }
Note that using subqueries can be slow. You may want to cache the latest play_id of a song as a field in the songs table to make the queries easier and faster. Start by adding a last_play_id field to your songs table.
class Play < ActiveRecord::Base
belongs_to :song
after_create do |play|
play.song.last_play_id = play.id
play.song.save!
end
end

You should probably look at named scopes.
class Song
named_scope :last_played_by, lambda {|call_sign| {:conditions => ["plays.radio_stations.call_sign = ?", call_sign]}}
end
You would use it as such
Song.last_played_by("WKRZ")
However, I realized that I don't know what mechanism you are using to find the last play. But you could split it up into two named scopes as such:
class Song
named_scope :played_by, lambda {|call_sign| {:conditions => ["plays.radio_station.call_sign = ?", call_sign]}}
named_scope :last_play {:conditions => ["MAX(plays.created_at)"]
end
To use it:
Song.last_play.played_by("WKRZ")
I believe that is correct, but I haven't tested it out and I'm not all that familiar with named scopes.

For performance, you should probably do what wvangergen suggests. But for syntatic sugar, I think this might work:
class Song
has_many :plays, :order => 'created_at' do
def last_played
self.maximum(:created-at)
end
end
end
class Play
named_scope :by, lambda {|call_sign| {:conditions => ["radio_stations.call_sign = ?", call_sign]}}
end
To use:
Song.plays.last_played.by("WKRZ")
I'm not testing the code out, so apologies for the syntax errors =) Now that I'm reading it, I don't think it will work, but I hope you might be able to figure it out or something.

I think you can do that starting with your associations (Note that the default 'order' is 'created_at'):
class Song
has_many :plays
has_many :radio_staions, :through => :plays, :uniq => true
has_one :last_radio_station, :through => :plays, :source => :last_radio_station
named_scope :last_played_at, lambda {|call_sign| {:include => :last_radio_station, :conditions => ["radio_stations.call_sign = ?", call_sign]}}
end
This expects your Play model to have:
class Play
has_one :last_radio_station, :class_name => 'RadioStation', :order => 'created_at'
has_many :radio_stations
Then you can simply call:
Song.last_played_at("WKRZ")
I haven't tried this myself, but hopefully it will work.

Created a named_scope that pulls all the plays for that particular radio station, with order 'created_at DESC' and then chain .last on to the end of the returned query. Along the lines of:
Song.played_by('WRXD').last

Related

includes / joins with has_and_belongs_to_many and sort / ordering on both models

I'm having a little bit of a brain problem with what I think would be a simple call:
I've got:
class Channel < ActiveRecord::Base
has_and_belongs_to_many :shows, :join_table => :channels_shows
end
class Show < ActiveRecord::Base
has_and_belongs_to_many :channels, :join_table => :channels_shows
end
A channel has a :position and :hidden in the database (:hidden can be false, or nil if not saved as I had forgotten about defaulting to 0).
A show has :approved (same as :hidden) and of course :created_at.
I want to be able to get Channels that are (:hidden => [nil, false] ) with each channels included Shows where a show is :approved and by created_at, newest first.
I can't figure out if this is a join or an include. The closest I've gotten is this, but this doesn't sort the included shows in the right order:
Channel.order('channels.position').where(:hidden => [nil, false] ).includes(:shows).where(shows:{approved: true})
Still looking at docs and trying things in the irb; feel like it's crazy simple but I'm just not getting it.
To sort the join records, just include that sort in the order clause after your primary sort. Your channels will still have the primary sort order, but when they are equal (ie when comparing the same channel but a different show), it will fall back to sorting by the second order (effectively sorting your included table):
Channel.order('channels.position, shows.created_at').includes(:shows)...
I think you should be able to do something like this:
class Channel < ActiveRecord::Base
has_and_belongs_to_many :shows, :join_table => :channels_shows
has_and_belongs_to_many :active_shows, :class_name => 'Show', :join_table => :channels_shows, :conditions => ["approved = ?", true], :order => "created_at desc"
end
To allow you to go
Channel.order('channels.position').where(:hidden => [nil, false] ).includes(:active_shows)
This is rails 3 syntax by the way.

Rails 3.1 - Simple search across three (or more) models?

I have three models that i'd like to perform a simple search across:
class Release < ActiveRecord::Base
has_many :artist_releases
has_many :artists, :through => :artist_releases
has_many :products, :dependent => :destroy
end
class Product < ActiveRecord::Base
belongs_to :release
has_many :artists, :through => :releases
end
class Artist < ActiveRecord::Base
has_many :artist_releases
has_many :releases, :through => :artist_releases
end
In my Product Controller i can successfully render a product list searching across release and product using:
#products = Product.find(:all, :joins => :release, :conditions => ['products.cat_no LIKE ? OR releases.title LIKE ?', "%#{params[:search]}%","%#{params[:search]}%"])
I really need to be able to search artist as well. How would I go about doing that? I ideally need it within the Product Controller as it's a product list I need to display.
I've tried adding :joins => :artist and variations thereof, but none seem to work.
I'm aware there are options out there like Sphinx for a full search, but for now I just need this simple approach to work.
Thanks in advance!
if you only want products back, just add both joins:
#products = Product.joins(:release,:artists).where('products.cat_no LIKE :term OR releases.title LIKE :term OR artists.name LIKE :term', :term => "%#{params[:search]}%").all
You may also need group_by to get distinct products back.
if you want polymorphic results, try 3 separate queries.
I know I'm suggesting a simple approach (and probably not the most efficient) but it will get your job done:
I would create a method in your Product model similar to this:
def find_products_and_artists
results = []
Product.find(:all, :conditions => ['products.cat_no LIKE ?', "%#{params[:search]}%"]).each do |prod|
results << prod
end
Release.find(:all, :conditions => ['releases.title LIKE ?', "%#{params[:search]}%"]).each do |rel|
results << rel
end
Artist.find(:all, :conditions => ['artist.name LIKE ?', "%#{params[:search]}%"]).each do |art|
results << art
end
return results
end
Then when you call it the method and store the returned results in a variable (e.g. results), you can check what object each element is by doing
results[i].class
and can make your code behave accordingly for each object.
Hope I helped.

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])

Rails Error: joins + include

I have the following models:
class Person < ActiveRecord::Base
has_many :images
has_one :preference
end
class Image < ActiveRecord::Base
belongs_to :person
end
class Preference < ActiveRecord::Base
belongs_to :person
end
I am trying to fetch all images that are public and at the same time eager load the people who own those images:
Image.find(:all, :conditions => ["images.person_id = ? AND preferences.image_privacy = ?", user.id, PRIVACY_PUBLIC],
:joins => [:person => :user_preference], :include => :person)
It appears Rails does not like the :include (I believe because :person is referenced in 2 models). This is the error I get (which disappears when I drop the :include option):
"ActiveRecord::StatementInvalid: Mysql::Error: Not unique table/alias: 'people'"
I can get around this by writing out the actual JOIN command as a string and passing it into the :include option, but this not Rails-y so I was hoping there's a cleaner way to do this.
Any help would be much appreciated.
Thanks!
It looks like you call it "preferences", and not user_preferences. So you join should be:
:joins => [:person => :preference])
By using JOIN you are actively including the People table, so you shoudn't need to add "include" again. This should work:
Image.find(:all, :conditions => ["images.person_id = ? AND preferences.image_privacy = ?", user.id, PRIVACY_PUBLIC],
:joins => [:person => :user_preference])
It might be the issue with Table Aliasing, rails doc has great details in Table Aliasing
also, post SQL here will be useful too.
You wrote :conditions => ["images.person_id", user.id] and you said that you want to load images and people who owns those images. But it looks like you are loading images that belongs to one person (not to group of people), because you specify only one user.id.
I would do it this way:
Person.find(user.id, :include => [:images, :preference], :conditions => ["preferences.image_privacy = ?", PRIVACY_PUBLIC])
It will load person and his/her images.
Probably I don't understand your problem correctly, because what I think you want to do doesn't seem logic to me.
Instead of using conditions you can try named_scope

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