Rails eager loading of counts? - ruby-on-rails

Eager loading is nice with the include attribute
Post.find(:all, :include => :author)
I'm wondering if you can also eager load counts, like if I wanted to get the number of comments for each post, without loading all the comments themselves?
Maybe something like
Post.find(:all, :include => [:author, "count(comments)")
I suppose I could use a count_cache column. But doing it all in an include would be really beautiful, if it's possible.
Extra bonus points if someone can show how to not only get the count, but put some conditions too, like count of only posts which have been approved.

they should already be loaded use
post.comments.length
I was having this same problem because I was using .count

Building off of avaynshtok's answer, the following technique should just make 2 database calls.
# ./app/controllers/posts_controller.rb
def index
# First load the posts
#posts = Post.all
# Then you can load a hash of author counts grouped by post_id
# Rails 4 version:
#comment_counts = Comment.group(:post_id).count
# Rails 3 version:
# #comment_counts = Comment.count(:group => :post_id)
end
Then in your view
<!-- ./app/views/posts/index.html.erb -->
<% #posts.each do |post| %>
<!-- reference the count by the post.id -->
post_count: <%= #comment_counts[post.id] %>
<% end %>

Try this:
Comment.count(:group => :post)
To filter by conditions:
Comment.count(:group => :post, :conditions => {:approved => true })
These will return hashes with posts as keys and the number of comments as values.

I just ran into this challenge and solved it this way:
def trainee_counts
#trainee_counts ||= Hash[Trainee.group(:klass_id).count]
end
# where the count is needed
trainee_counts[klass_id].to_i
One call to database and does not load trainees.

In MySQL at least, it is faster to do these as two separate calls because you get to avoid the join. I know this doesn't answer your question, but it seems like you're trying to get better speed and doing
Post.find ...
Then
post.comments.count
Is faster and more memory efficient (for the database) than if you retrieve both in one query.

Related

Is this way of calling object supposed to be bad practice when considering loading speed?

My way
controller pattern 1 (note: Here, it's calling all users!!)
#users = User.confirmed.joins(:profile)
view pattern 1 (note: Here, it only shows first 10 users but it show the number of all users!!)
<%= "ALL ("+ #users.count.to_s + " users)" %>
<% #users.limit(10).each do |users| %>
<%= render 'users/user', :user => users %>
<% end %>
Should it be just like this below if I'm considering page loading speed?
Or it won't be changed?
controller pattern 2 (note: I added limit(10), and #users_count to count all users)
#users = User.confirmed.joins(:profile).limit(10)
#users_count = User.confirmed.joins(:profile).count
view pattern 2 (note: I took it off limit(10) and use #users_count for count)
<%= "ALL ("+ #users_count.to_s + " users)" %>
<% #users.each do |users| %>
<%= render 'users/user', :user => users %>
<% end %>
If you have lazy loading disabled, then the second approach would be faster because Rails doesn't need to fetch all records from the database. You should really fetch only the records you need when performing queries.
If you have lazy loading enabled (by default), then it is the same, because the data is fetched when it is needed, so the effect will be the same. You can also put two variables in controller and write the same query as you did in the view and the data will be fetched only if and when it is needed.
#users = User.confirmed.joins(:profile)
#users_count = #users.count
#users = #users.limit(10)
You can check sql generated by the app in your rails console and then decide.
Also, if you are using profile in user.html.erb, consider using includes instead of join. Join can cause n+1 problem if you need associated records. If you don't, you do not want to fetch records you don't need. You can read more about it here, in 12 Eager Loading Associations.
The two options are exactly the same. Neither of them loads all the Users because you're just chaining scopes. The query is only run when you call .each in the view, at which point you've applied the .limit(10) anyway. I'd go with the first option because the code is cleaner.
#users.count does one query to get the count, it doesn't instantiate any User objects.
#users.limit(10).each ... does one query (actually two because you've used includes) with a limit, so it will instantiate 10 objects plus your includes.
you can try #users.find_in_batches
Please take a look
Find in batches
Please let me know
If you want speed loading
I can suggest you memcache Memcache

ActiveRecord - how to do includes after the query was executed?

In a scenario with 1->N->N assocations. For example: Post->Comments->Votes (votes will be list of names of people who voted on the comment). To display a page the query with includes might look like:
#post = Post.where(:id => 100).includes({:comments => :votes}).first
I am starting to add caching support. Which means if the comments partial is already cached I will not need to run include the comments/votes all the time. So I wonder if there is a way to make the code appear like:
# controller
#post = Post.find(100)
# view
<% cache('comments', #post.last_comment_time do %>
<% #post.includes({:comments => :votes}).comments.each do |comment| # ???? %>
<% end %>
Running the "post-query" includes, will "fill in" the associations. So #post.comments will be populated and each comment will include all the votes. Is there a way to achieve this?
P.S. I am aware the view is not the best place to run the query, this is just an example.
in latest releases of rails, all the finder-methods return a proxy object, that will only trigger a database-call once you send it some iterator-method like all or first in your case. this is why you can chain all the calls like Post.where.order.sort.bla.
it's not possible though to load the post model and use an includes call later. includes works by using a join call on the relations that get loaded with the model instance, so that you have just one database-call instead of one for each relation.
executing active_record code in your view is also a bad practice. the data-retrieval is the responsibility of the controller, not the view.
This is a fairly old question but this can be done now like this
# controller
#post = Post.find(100)
# view
<% cache('comments', #post.last_comment_time do %>
<% ActiveRecord::Associations::Preloader.new.preload #post, comments: :votes # this will trigger one query %>
<% #post.comments.each do |comment| # this will not trigger any additional queries %>
<% end %>
Not the cleanest way but it does the job

How can I search across multiple tables(models) with Sunspot Solr?

I want users to be able to use one search box to search for different objects. I would differentiate them on the results page. Both would be full text search.
Should I make a Search controller and load everything into the index action with something like:
#posts = Post.all
#groups = Group.all
Something tells me that would be fantastically inefficient.
I'm not really sure where to start, I haven't managed to find anything addressing this question on the interwebs, but if I have overlooked something let me know.
Thanks
EDIT:
here's my search bar that is available globally on my website:
-form_tag posts_path, :method => :get do
=text_field_tag :search, params[:search], :id => 'searchfield'
=submit_tag '',:name => nil, :id => 'searchbutton'
it only searches the "Post" model right now, and displays the results on the Post#index view
I want to be able to have queries typed into the search box be searched for in both the Post and Group tables, and the results be displayed in two separate columns on the same page. maybe through a search controller/view
If you want to search both types at once, you can use this form:
# Pass your models in by class constant
Sunspot.search(Post,Group) do |s|
s.fulltext(params[:search])
end
This is documented in the wiki:
https://github.com/sunspot/sunspot/wiki/Working-with-search#initiating-a-search
Add the searchable directive from sunspot solr to your models for indexing. For example:
class Post < ActiveRecord::Base
searchable do
text :title, :body
end
end
class Group < ActiveRecord::Base
searchable do
text :name
end
end
If you have existing data in DB make sure to run rake sunspot:solr:reindex for indexing. For new data the indexing will be done in a hook.
Now you can search:
#posts = Post.search {fulltext params[:search]}
#groups = Group.search {fulltext params[:search]}
Now you have the data for your two columns.
This is not the answer, but I found something useful. It is a gem that helps you searching across multiple tables:
https://github.com/toptierlabs/acts_as_fulltextable.
It helps you keeping all the searchable data (from different models) in one place.

Rails, is it ok to look up objects in views?

A controller points to a view. In that view is it acceptable to find objects <% #XXX = XXXX.where(..... %> or is that bad?
Trying to work through performance issues which is why I ask. Thanks
Putting query logic in the model has more to do with maintainability then it does with performance. Since most of the ActiveRecord/ARel logic deals with lightweight relation objects that only trigger the actual query based on certain methods, generally those provided via Enumerable (each/map/inject/all/first), which are usually called from the view anyway, the actual query gets triggered in the view, and not anywhere else.
Here's an example of the difference between limit(3) and first(3) from an app I'm working on atm.
ruby-1.9.2-p180 :018 > PressRelease.limit(3).is_a? ActiveRecord::Relation
=> true
ruby-1.9.2-p180 :019 > PressRelease.first(3).is_a? ActiveRecord::Relation
PressRelease Load (2.8ms) SELECT "press_releases".* FROM "press_releases" ORDER BY published_at DESC
=> false
As you can see, limit does not actually trigger a query, where first does.
When it comes to performance you are usually trying to ensure that your queries are not executed in your controller/model so that you can wrap them in a cache block within your view, thus eliminating that query from most requests. In this case you really want to make sure your not executing the query in your controller by calling any of the Enumerable methods.
A quick example of a blog that lists the last 10 blog posts on the home page that is setup with caching might look like this.
# app/controllers/posts_controller.rb
class PostsController < ApplicationController
def index
# Something like this would trigger the query at this point and should be
# avoided in the controller
# #posts = Post.first(10)
# So #posts here will be the Relation returned from the last_ten scope, not
# an array
#posts = Post.last_ten
end
...
end
# app/models/post.rb
class Post < ActiveRecord::Base
# Will return an ActiveRecord::Relation
scope :last_ten, order('created_at DESC').limit(10)
end
# app/views/posts/index.html.erb
<ul>
# The query will actually trigger within the cache block on the call to each,
# preventing the query from running each time and also reducing the template
# rendering within the cache block.
<%= cache(posts_cache_key) do %>
<% #posts.each do |post| %>
..
<% end %>
<% end %>
</ul>
For clarity, all this would be the exact same as doing
# app/views/posts/index.html.erb
<ul>
<%= cache(posts_cache_key) do %>
<% Post.order('created_at DESC').limit(10).each do |post| %>
...
<% end %>
<% end %>
</ul>
Except that if you now want to modify the logic for how it pulls the query, say you wanted to add something like where(:visible => true).where('published_at' <= Time.now) your jumping into your view instead of making changes in the model where the logic should be. Performance wise the difference is insignificant, maintenance-wise the latter turns into a nitemare rather quickly.

Refactoring a simple method in my controller

I'm having a tough time deciding how to refactor this method in my controller. The idea is that (in this case) it graphs the users that joined (or were created) in the past two weeks.
You might be wondering why I did the #graph_limit thing, and that is because I always want the day that has the most results to be the tallest bar on my bar chart (which in the view are just created with css by making the height of the <div> using css).
Basically I want to dry it up and... ya know just about improve this method as much as possible:
# Controller
def index
two_weeks_ago = Date.today - 13.days
#users_graphed = User.count(:conditions=>["created_at >= ?", two_weeks_ago], :order => 'DATE(created_at) DESC', :group => ["DATE(created_at)"])
two_weeks_ago.upto(Date.today) do |day|
#graph_limit = 100/#users_graphed.values.max.to_f
#users_graphed[day.to_s] ||= 0
end
end
Also I should mention, that you guys are probably going to rip my code to shreds... so I'm bracing for the outcome.
# View
<% #users_graphed.sort.reverse.each do |user| %>
<li>
<% content_tag :div, :style => "height: #{number_with_precision(user[1] * #graph_limit, :precision => 2)}px; ", :class => "stat_bar" do %>
<%= content_tag(:span, user[1]) unless user[1] == 0 %>
<% end %>
</li>
<% end %>
Ultimately and what my real goal here is to put this into my application controller and be able to chart any models by it's create_at times. maybe something like tasks.chart_by(2.weeks). How would you guys get this separated out into something I can use throughout the whole app?
I agree with Joseph that your controller here is doing a lot of work that should be done in the model. Any time you're specifying multiple find parameters in your controller, ask yourself whether or not that should be in your model instead.
You're doing a lot of iterating here that seems needless. Firstly, You shouldn't be calculating #graph_limit inside the loop. You're recalculating it 14 times, but the value is going to be the same every time. Do that outside the loop.
Secondly, that sort.reverse in your view sticks out. You're already sorting in your finder (:order => 'DATE(created_at) DESC'), and then you're sorting again in your view and then reversing it? You should instead be asking the database for the values in the final order you want them. Then to make your zero-filling code work you can just reverse it, doing Date.today.downto(two_weeks_ago) instead of upto.
I would say that you should really be doing this all in SQL, but unfortunately (as perhaps you've discovered) MySQL makes it difficult to fill in missing days without creating a calendar table to join against.
Thanks Jordan, per your ideas (which were really great by the way) I've created a helper that is like such:
def graph_by_time(klass, time_ago)
time_range_start = Date.today - time_ago
#elements_graphed = klass.count(:conditions=>["created_at >= ?", time_range_start], :order => 'DATE(created_at) DESC', :group => ["DATE(created_at)"])
#graph_limit = 100/#elements_graphed.values.max.to_f
time_range_start.upto(Date.today) do |element|
#elements_graphed[element.to_s] ||= 0
end
return #elements_graphed.sort.reverse
end
The biggest issue here is zero filling the days which have no records associated with them, your method of switching to from upto to downto didnt work and only returned the records which did result in a integer other than zero.

Resources