I'm new to ORM in general and I'm having trouble getting Rails to generate ok SQL. I have two tables, messages and users. Basically, I want a list of the messages with the user_name of the senders tacked on.
It would be great if:
I only got that one column from the user table (the user table has lots of columns that don't need to get selected)
It used a LEFT JOIN (I want messages even if the users are gone for some reason, and I don't want to slow things down with an INNER JOIN)
I don't need to mention the names of the foriegn keys in this code (I should only have to set those up in the models and "NOT REPEAT MYSELF", right?)
I have these models set up:
class Message < ActiveRecord::Base
belongs_to :sender, :class_name => "User", :foreign_key => "from_user_id"
belongs_to :recipient, :class_name => "User", :foreign_key => "to_user_id"
end
class User < ActiveRecord::Base
has_many :sent_messages, :class_name => "Message", :foreign_key => "from_user_id"
has_many :received_messages, :class_name => "Message", :foreign_key => "to_user_id"
end
I really WANT to like ORM, but it seems like it's so easy to get to the point where you just have to write the queries yourself.
Perhaps something like this will do the trick (in Message model):
default_scope select("messages.*, users.name as user_name").joins(:user).includes(:user)
The thing is, that unless this join works as expected, you will need to specify the joins() with an SQL snippet, and then the includes() will probably NOT utilize this join, and rather do N+1 queries in total.
Related
First, the topic.
I have three models, which are linked between each other with a has_many :trough association like this:
#User model
has_many :chars_del, :class_name => CharDelegated, :dependent => :destroy
has_many :chars, :through => :chars_del
#CharDelegated model
#has a field owner:integer
belongs_to :char
belongs_to :user
#Char model
#has fields name:string
has_many :chars_del, :class_name => CharDelegated
has_many :users, :through => :chars_del
What I need to do is I need to search from a User Record to find all the Chars that the particular user ownes (:owner field is true) ordered by name. I have been stuck with this for a couple hours now, so I believe that I could have missed a very simple answer... But nothing that I have tried so far did work even a bit.
UPDATE
found something that works:
user.chars.where(:char_delegateds => {:owner => 1}).order('name')
don't know why the :chars_del gave an error, but the full table name did the job.
Andrew, your answer works well too and is a little faster on the database, thans alot.
Does
user.chars.order('name')
not work? (Given user is a single User instance.)
Edit
Given your new information:
CharDelegated.where(user_id: user.id, owner: true).map(&:char)
should work.
In your specific example you don't need to search through the middle table but if you want to see an example of how to use the joining table and search through it for a more complex scenario you can do it this way.
#char = Char.all(:include => :users, :conditions => ["char_delegated.user_id in (?)", user_id]).order('name')
I have a pretty complicated set of models and I'm trying to figure out how to set it all up so that rails will generate chained queries (though I'm not sure it is possible). So my question ends up being: how do I chain these together OR how do I properly paginate this type of function?
First, the high overview.
It is a "social network" and I have users who can be students or teachers. Students and teachers can both follow each other. Both can also do things on the site that result in activity_items. A students can belong to a teacher. Therefore, if a student is following a teacher, they should also see the activity_items of the students under that teacher.
And that is where it gets complicated. I am trying to figure out how to display an activity feed.
Now the models:
model ActivityItem < ActiveRecord::Base
belongs_to :profile, :polymorphic => :true
attr_accessor :posted_by_user
end
model Following < ActiveRecord::Base
belongs_to :profile, :polymorphic => true
belongs_to :following, :polymorphic => true
end
model Student < ActiveRecord::Base
has_many :following_relationships, :as => :profile, :class_name => "Following"
has_many :follower_relationships, :as => :following, :class_name => "Following"
# getting the actual student/teacher model for each relationship
def following
self.following_relationships.reset
following_relationships.collect(&:following)
end
def activity_feed
all = following
all.inject([]) do |result,profile|
result | profile.activity_items
end
end
end
model Teacher < ActiveRecord::Base
[same as student, really it is in an Extension]
end
So the method I'm most concerned about is activity_feed. Without running the large set of queries every time, I'm not sure how to paginate the data. I have a few more methods that go through each activity_item and set posted_by_user to true or false to let me know if it is an activity_item created by someone they are following directly or indirectly (through the teacher).
How can I modify this so that I'm able to either
(A) get a single (or two or three) queries so that I can do a pagination method on it.
(B) how would I properly cache / paginate a large data set like this?
I'm building a blog-type website in Rails (3) where "Authors" can follow other authors and in order and in order to find authors with the most followers I wrote this scope:
scope :popular, all.sort { |a, b| a.followers.count <=> b.followers.count }
I know this is really inefficient as it has to perform multiple queries for each author in the database and then compares the result. I'm asking this question to see if there isn't a better way of doing this. The relevant code is as follows:
class Author < ActiveRecord::Base
has_many :relationships, :dependent => :destroy, :foreign_key => "follower_id"
has_many :following, :through => :relationships, :source => :followed
has_many :reverse_relationships, :dependent => :destroy, :foreign_key => "followed_id", :class_name => "Relationship"
has_many :followers, :through => :reverse_relationships, :source => :follower
scope :popular, all.sort { |a, b| a.followers.count <=> b.followers.count }
end
The "followers" are implemented through a separate model:
class Relationship < ActiveRecord::Base
belongs_to :follower, :class_name => "Author"
belongs_to :followed, :class_name => "Author"
end
And for retrieving popular authors:
#popular_authors = Author.popular[0..9]
I'd love to be able to write something like Author.popular.limit(10) instead, but I understand that in order for this to work, Author.popular has to return an object of the ActiveRecord::Relation class, rather than an array.
As I mentioned, this code does work, but it's horribly inefficient, issuing hundreds of queries just to find the top 10 authors. Any help would be greatly appreciated!
One optimization might be to use eager loading. I suspect you have many queries that say, SELECT * fromAuthorWHEREfollower_id= 7
Eager loading can turn your hundreds of queries into a giant one for you. This might generate a slow query, but often times will faster because the time for the slow query was less than the 5000 fast queries.
I am not a SQL guru, but you might also want to use a GROUP_BY with LIMIT = 10. to get the 10 most popular.
Try something like
Authors.find(:all, :include => Authors, :group_by => " SELECT `count` as (some subquery I don't know to get the number of followers)", :limit => 10)
Scroll down to Eager loading of associations
Imagine something like a model User who has many Friends, each of who has many Comments, where I'm trying to display to the user the latest 100 comments by his friends.
Is it possible to draw out the latest 100 in a single SQL query, or am I going to have to use Ruby application logic to parse a bigger list or make multiple queries?
I see two ways of going about this:
starting at User.find and use some complex combination of :join and :limit. This method seems promising, but unfortunately, would return me users and not comments, and once I get those back, I'd have lots of models taking up memory (for each Friend and the User), lots of unnecessary fields being transferred (everything for the User, and everything about the name row for the Friends), and I'd still have to step through somehow to collect and sort all the comments in application logic.
starting at the Comments and using some sort of find_by_sql, but I just can't seem to figure out what I'd need to put in. I don't know how you could have the necessary information to pass in with this to limit it to only looking at comments made by friends.
Edit: I'm having some difficult getting EmFi's solution to work, and would appreciate any insight anyone can provide.
Friends are a cyclic association through a join table.
has_many :friendships
has_many :friends,
:through => :friendships,
:conditions => "status = #{Friendship::FULL}"
This is the error I'm getting in relevant part:
ERROR: column users.user_id does not exist
: SELECT "comments".* FROM "comments" INNER JOIN "users" ON "comments".user_id = "users".id WHERE (("users".user_id = 1) AND ((status = 2)))
When I just enter user.friends, and it works, this is the query it executes:
: SELECT "users".* FROM "users" INNER JOIN "friendships" ON "users".id = "friendships".friend_id WHERE (("friendships".user_id = 1) AND ((status = 2)))
So it seems like it's mangling the :through to have two :through's in one query.
Given the following relationships:
class User < ActiveRecord::Base
has_many :friends
has_many :comments
has_many :friends_comments, :through => :friends, :source => :comments
end
This statement will execute a single SQL statement. Associations essentially create named scopes for you that aren't evaluated until the end of the chain.
#user.friends_comments.find(:limit => 100, :order => 'created_at DESC')
If this is a common query, the find can be simplified into its own scope.
class Comments < ActiveRecord::Base
belongs_to :user
#named_scope was renamed to scope in Rails 3.2. If you're working
#if you're working in a previous version uncomment the following line.
#named_scope :recent, :limit => 100, : order => 'created at DESC'
scope :recent, :limit => 100, :order => 'created_at DESC'
end
So now you can do:
#user.friends_comments.recent
N.B.: The friends association on user may be a cyclical one through a join table, but that's not important to this solution. As long as friends is a working association on User, the preceding will work.
I'm trying to understand how ActiveRecord deals with associations that are more complex than simple has_many, belongs_to, and so on.
As an example, consider an application for recording music gigs. Each Gig has a Band, which has a Genre. Each Gig also has a Venue, which has a Region.
In the rough notation of MS Access (which I'm suddenly beginning to feel quite nostalgic for) these relationships would be presented like this
1 ∞ 1 ∞ ∞ 1 ∞ 1
Genre ---- Band ---- Gig ---- Venue ---- Region
I would like to be able to find out, for example, all the bands who've played in a region, or all the venues that host a certain genre.
Ideally, my models would contain this code
class Genre
has_many :bands
has_many :gigs, :through => bands
has_many :venues, :through => :gigs, :uniq => true
has_many :regions, :through => :venues, :uniq => true
end
class Band
belongs_to :genre
has_many :gigs
has_many :venues, :through => :gigs, :uniq => true
has_many :regions, :through => :venues, :uniq => true
end
class Gig
belongs_to :genre, :through => :band
belongs_to :band
belongs_to :venue
belongs_to :region, :through => :venue
end
and so on for Venue and Region.
However, it seems I have to produce something like this instead
class Genre
has_many :bands
has_many :gigs, :through => bands
has_many :venues, :finder_sql => "SELECT DISTINCT venues.* FROM venues " +
"INNER JOIN gigs ON venue.id = gig.venue_id " +
"INNER JOIN bands ON band.id = gig.band_id " +
"WHERE band.genre_id = #{id}"
# something even yuckier for regions
end
class Band
belongs_to :genre
has_many :gigs
has_many :venues, :through => :gigs, :uniq => true
# some more sql for regions
end
class Gig
delegate :genre, :to => :band
belongs_to :band
belongs_to :venue
delegate :region, :to => :venue
end
I have two questions - one general and one particular.
The general:
I would have thought that what I was trying to do would come up fairly often. Is what I have really the best way to do it, or is there something much simpler that I'm overlooking?
The particular:
What I have above doesn't actually quite work! The #{id} in the second genre model actually to return the id of the class. (I think). However, this seems to work here and here
I realise this is a rather epic question, so thank you if you've got this far. Any help would be greatly appreciated!
Associations are designed to be readable and writable. A large part of their value is that you can do something like this:
#band.gigs << Gig.new(:venue => #venue)
It sounds, though, like you want something that's read-only. In other words, you want to associate Venues and Genres, but you'd never do:
#venue.genres << Genre.new("post-punk")
because it wouldn't make sense. A Venue only has a Genre if a Band with that particular Genre has a Gig there.
Associations don't work for that because they have to be writable. Here's how I'd do readonly associations:
class Genre
has_many :bands
def gigs
Gig.find(:all, :include => 'bands',
:conditions => ["band.genre_id = ?", self.id])
end
def venues
Venue.find(:all, :include => {:gigs => :band},
:conditions => ["band.genre_id = ?", self.id])
end
end
You can add conditions and parameters to your associations.
Recent versions of ActiveRecord give the power of named_scopes, which will work on associated records as well.
From a current project
Folder has_many Pages
Page has_many Comments
# In Page
named_scope :commented,
:include => "comments",
:conditions => ["comments.id IS NULL OR comments.id IS NOT NULL"],
:order => "comments.created_at DESC, pages.created_at DESC"
Using this we can say:
folder.pages.commented
Which will scope on the associated records, doing a conditional with the supplied parameters.
Plus! named_scopes are composable.
And more scopes:
named_scope :published, :conditions => ["forum_topics.status = ?", "published"]
And chain them together:
folder.pages.published.commented
For associations like this, you're going to end up writing custom SQL -- there's no real way that you can handle a chain of associations like this without having to do some fairly massive joins, and there really isn't an efficient way for the built-in query generators to handle it with a one-liner.
You can look into the :joins parameter of ActiveRecord as well -- this may do what you want.
Sounds like a job for nested_has_many_through! Great plugin that allows you to do nested has_many :throughs