Rails Error: joins + include - ruby-on-rails

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

Related

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 MySQL include restrictions

class Comment < ActiveRecord::Base
belongs_to :post
belongs_to :user
end
class Post < ActiveRecord::Base
has_many :comments
belongs_to :user
end
class User < ActiveRecord::Base
has_many :posts
has_many :comments
end
I'm trying to pull out Post data, while eager loading the User and Comment data as well, but with the limitation of not loading the Comments which have been blocked (TINYINT field in the Comment table). The following works when there are comments present, but it's causing issues when I load posts that don't have any comments yet:
#post = Post.find(params[:id],
:include => {:comments => :user},
:conditions => "comments.blocked = 0")
Any suggestions on how I can run this query such that it will work when no comments are present? Thanks.
What error does it give when you try to do that on a post that has no comments?
Update:
What about this variation?
#post = Post.find(params[:id],
:include => {:comments => :user},
:conditions => {:comments => {:blocked => false}})
Conditions on eagerly loaded associations is sort of unusual. Perhaps you should be using the :joins option instead? Or skip eagerly loading the post (since it's just a single one), and have a named scope for the non-blocked comments to use in your view. Something like this perhaps:
#post = Post.find(params[:id])
#comments = #post.comments.where(:blocked => false).all(:include => :user)
(Just typing off the cuff here, not certain that's exactly the right syntax for you)
OK, so after a bit of fiddling, here's the answer (which may seem obvious to some, but the count() function in MySQL was giving me some grief):
#post = Post.find(params[:id],
:include => {:comments => :user},
:conditions => "comments.blocked = 0 OR posts.comments_count = 0")
comments_count is a counter_cache field, so I'm getting around the explicit use of the MySQL count() function. It feels like a kludge, but for now I'm OK with that. If anyone has a more elegant solution, please let me know!

how do I join and include the association

How do I use both include and join in a named scope?
Post is polymorphic
class Post
has_many :approved_comments, :class_name => 'Comment'
end
class Comment
belongs_to :post
end
Comment.find(:all, :joins => :post, :conditions =>
["post.approved = ? ", true], :include => :post)
This does not work as joins does an inner join, and include does a left out join.
The database throws an error as both joins can't be there in same query.
Have you yet tried simply omitting the :joins part of the ActiveRecord call? In my test case of a has_many association, :include will use a join if your conditions refer to the included table name. For example,
Comment.all :include => :post
will run two queries total: one for comments, and one for their posts. The Rails team says that that is better performance. However, if Rails detects that your conditions need a join,
Comment.all :include => :post, :conditions => ['post.approved = ?', true]
will run one query, since you need it.
Isn't ActiveRecord so smart?
If you want to get all the approved Posts with their Comments you can just do something like this:
Post.find(:all, :include => "comments", :conditions => ['approved = ?', true])
This will give you all the approved posts with all the comments.
You should not probably use join as it will result in too huge data set (product columns will be included for each comment related to it).

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

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

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

Resources