Rails activerecord order by field in related table - mysql

I have a typical forum style app. There is a Topics model which has_many Posts.
What I want to do using Rails 2.3.x is query the topics table and sort by the most recent post in that topic.
#topics = Topic.paginate :page => params[:page], :per_page => 25,
:include => :posts, :order => 'HELP'
I'm sure this is a simple one but no joy with Google. Thanks.

Sorting on a joined column is probably a bad idea and will take an enormous amount of time to run in many situations. What would be better is to twiddle a special date field on the Topic model when a new Post is created:
class Post < ActiveRecord::Base
after_create :update_topic_activity_at
protected
def update_topic_activity_at
Topic.update_all({ :activity_at => Time.now }, { :id => self.topic_id})
end
end
Then you can easily sort on the activity_at column as required.
When adding this column you can always populate the initial activity_at with the highest posting time if you have existing data to migrate.

Related

(Rails) Does scoping apply when calling model.association.find?

For example, I have:
class School < ActiveRecord::Base
has_many :students
end
one_school = School.first
Is there a speed difference between using:
Student.find :all, :conditions => { :first_name => "John", :school_id => one_school.id }
and
one_school.students.find :all, :conditions => { :first_name => "John" }
I was wondering if calling "one_school.students.find" will iterate through ALL the student records, or will they iterate through the student records belonging to one_school only?
This is more of a question on performance. I need to know if the latter query really is faster in rails.
That's basically the same (Rails will use a join in both cases), you can see the query and it's performance in your server console and compare. To enhance performance make sure you index school_id on your students table.

Reducing queries - conditions on associations?

I need some help optimizing the following method. The queries have become too costly as my database has grown in size to 3K users and 100K workouts. Unfortunately my SQL skills are rudimentary.
The method returns the rank of a given workout against other workouts by people of the same gender, and the total number of workouts for that gender. There are other applicable conditions to the workouts which I have removed to simplify this sample:
Class Person
has_many :workouts
named_scope :men, :conditions => {:male => true}
end
Class Workout
belongs_to :person
named_scope :by, lambda {|person_id| {:conditions => {:person_id => person_id}}}
def rank_by_gender
people = Person.men.collect{|p| p.id}
{ :place =>
Workout.by(people).count(:conditions => ["time < ?", self.time]) + 1
:entries =>
Workout.by(people).count() }
end
end
Person.men now returns several thousand records. Is there a way to combine that query with the count query, possibly by using a join on the people table with on the :male attribute?
Is there a way to put a condition on the Person association into the conditions passed to the count method?
Beyond that, does anyone see other ways to reduce the size and time of database queries in this method?
This should work for you:
{
:place => Workout.joins(:person).where(["`workouts`.time < ? AND `people`.male = ?", self.time, true]).size,
:entries => Workout.joins(:person).where(:people => {:male => true}).size
}

Sorting a Rails database table by a column in an associated model

I'm trying to implement Ryan Bates' sortable table columns code (Railscast #228) but I'd like to be able to sort on an associated column. In particular, I have the following models and associations:
class Project < ActiveRecord::Base
belongs_to :program_manager, :class_name => "User"
class User < ActiveRecord::Base
has_many :program_manager_projects, :class_name => "Project", :foreign_key => "program_manager_id"
The association between the Project model and the User model is mediated by the 'program_manager_id' foreign key, which the user sets in the new/edit views using a collection-select dropdown. Here's part of the annotation at the top of project.rb:
# Table name: projects
# program_manager_id :integer
I want to be able to sort my list of projects in the index view by the program manager's name, i.e., by project.program_manager.name.
Ideally, I'd be able to point :order to this name somehow, perhaps with something like this in the index method of my ProjectsController:
#projects = Project.find(:all, :order => project.program_manager.name)
But that obviously won't work (not to mention Ryan's routine implements this with a specific reference to table names from the model to be sorted.)
I've come across some intimidating approaches that use named_scope, such as:
named_scope :most_active, :select => "questions.*", :joins => "left join comments as comments_for_count on comments_for_count.question.id = questions.id", :group => "questions.id", :order => "count(questions.id) desc"
But given my lack of MySQL expertise, this is fairly impenetrable to me.
Can anyone help me either generalize the named_scope example above for my specific case, or point me to a more straightforward strategy?
Thanks very much,
Dean
Let's dissect that named scope you referenced above. Imagine a model Question which has many Comments.
named_scope :most_active, :select => "questions.*", :joins => "left join comments as comments_for_count on comments_for_count.question.id = questions.id", :group => "questions.id", :order => "count(questions.id) desc"
:most_active
the name of your scope. You would reference thusly: Question.find(:all).most_active
:select => "questions.*"
by default scopes selects all columns from your table anyway, so this limits the results to only the questions table, and not the comments table. This is optional.
:joins => "left join comments as comments_for_count on comments_for_count.question.id = questions.id"
this is saying for every question, I also want to get all comments associated with them. The comments table has a column 'question_id' which is what we'll be using to match them up to the appropriate question record. This is important. It allows us access to fields that are not on our model!
:group => "questions.id"
This is required for the count() function in the order clause to tell us that we want the count of comments based on question. We don't need the count function in our order clause, so we also don't need this group statement
:order => "count(questions.id) desc"
Return the results in order of number of comments, highest to lowest.
So for our example, discarding what we don't need, and applying to your needs, we end up with:
:named_scope :by_program_manager_name, :joins => "left join users on projects.program_manager_id = users.id", :order => "users.name"
This named_scope would be called thusly:
Project.find(:all).by_program_manager_name
Note this is basically equivalent to:
Project.find(:all, :joins => "left join users on projects.program_manager_id = users.id", :order => "users.name")
But, as cam referenced above, you should really know the underlying SQL. Your abilities will be severely hampered without this understanding

Rails 3 - Eager loading with conditions

Okay, I'm thoroughly stumped on this one. I'm trying to build a menu of published web pages organized by category.
Category.rb:
belongs_to :parent, :class_name => "Category", :foreign_key => "parent_id"
has_many :children, :class_name => "Category", :foreign_key => "parent_id"
has_many :pages, :documents, :galleries
Page.rb
belongs_to :category
The Page model also has :is_published, so I'm trying to filter on that as well. I am reluctant to post my feeble query attempts, but see no other solution than to beg much smarter people:
(self is #current_website)
self.categories.includes(:children, :pages).where('pages.is_published = 1')
This returns mostly what I need, but not Parent Categories without published pages. For instance, it works great if I have:
Parent Category
- Published Page
- Child Category
-- Published Page
Where it fails is when I have no published pages in the parent, like this:
Parent Category
- Child Category
-- Published Page
- Child Category
-- Published Page
Thanks in advance for any help on this. I'm trying to learn as much as I can about queries, but I'm against the wall on this.
UPDATE: Implementing KandadaBoggu's suggestion has yielded much better results, this was added to Category.rb
has_many :published_pages, :class_name => "Page",
:conditions => {:is_published => true}
However, when using the following:
self.categories.where(:parent_id => nil).includes({:children => :published_pages},
:published_pages)
I get the results I need, but I also get empty Parent Categories (no published_pages, no child categories with published pages. An example:
- Parent Category
-- Published Page
- Parent Category
-- NOTHING
My temporary fix was to appended the query with:
reject{|category| category.pages.empty? && category.children.empty?}
Thanks again for your help.
Add a new association called published_pages (apart from your current associations)
class Category
has_many :children, :class_name => "Category",
:foreign_key => "parent_id"
has_many :published_pages, :class_name => "Page",
:conditions => { :is_published => true }
end
Now you can get all the categories as follows:
self.categories.includes(:children, :published_pages)
If you are interested in learning why your approach didnt work, read the Rails documentation (scroll 10-15 lines after the Eager loading of associations section). I have included the relevant snippet below:
For example
Post.includes([:author, :comments]).where(['comments.approved = ?', true]).all
This will result in a single SQL query with joins along the lines of:
LEFT OUTER JOIN comments ON comments.post_id = posts.id and
LEFT OUTER JOIN authors ON authors.id = posts.author_id.
Note that using conditions like this can have unintended consequences.
In the above example posts with notion approved comments are not returned
at all, because the conditions apply to the SQL statement as a whole
and not just to the association. You must disambiguate column
references for this fallback to happen, for example :order =>
"author.name DESC" will work but :order => "name DESC" will not.
To eager load filtered rows of an association, use an association with conditions:
class Post < ActiveRecord::Base
has_many :approved_comments, :class_name => 'Comment',
:conditions => ['approved = ?', true]
end
Post.find(:all, :include => :approved_comments)

Use :include statement to find records

I'm working on Ruby On Rails 2.3.2 and I'm learning how to use the :include statement.
I'd like to get all the announcements order by their rate. I've got two models and tables for that: Announcement and Rate.
The models look like this:
class Announcement <
ActiveRecord::Base
belongs_to :rate
end
class Rate < ActiveRecord::Base
belongs_to :announcement
end
I'd like to do something like the following, but using :include statement.
Announcement.find_by_sql 'select * from announcements ann inner join rates r on ann.id = r.announcement_id order by r.average DESC'
I already tried this:
Announcement.paginate :page => params[:page], :per_page => 10, :include => [:rates], :order => 'rates.average DESC'
but it's trying to associate rates.id = announcements.id, instead of rates.announcement_id = announcement_id
How can I specify the correct relationship to make this work?
As Yuri points out in the comments, you have incorrectly defined your relationship.
Announcements should have_one :rate
As a named scope on announcment:
class Announcement < ActiveRecord::Base
has_one :rate
named_scope :order_by_rate, :include => :rate, :order => 'rates.average DESC'
end
Announcement.order_by_rate.paginate :per_page => 10, :page => params[:page]
Note the difference between :include and :joins as a find option. :joins will join the on the association(s) or SQL JOIN fragment given for purposes of complex WHERE/ORDER clauses. :include will only works on associations, it provides the same advantages of :joins, and also eager load the association.
Where eager loading makes a difference:
With include, #a.rate is populated in the same SQL statement that populates #a.
#a = Association.first :include => :rate
Without include #a.rate is not populated until required.
#a = Association.first
#a.rate #=> #a.rate is populated here, with a separate SQL statement.