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.
Related
Having trouble understanding how using links_to filter content within the same controller in the rails view works. My code is below:
# index.html.erb (link nav area)
<nav>
<%= link_to 'Daily Monitoring', root_path(:category => "dailymonitoring") %>
<%= link_to 'Smoke Tests', root_path(:category => "smoketests") %>
</nav>
# index.html.erb (cont.)
<ul id="results">
<% if #reportlinks == "dailymonitoring" %>
# dailymonitoring content
<% elsif #reportlinks == "smoketests" %>
# smoketests content
<% end %> <!-- end conditional -->
</ul>
# reports_controller.rb
if params[:category]
#reportlinks = Report.where(:category => params[:category])
else
#reportlinks = Report.all
end
# model (report.rb)
class Report
include ActiveModel::Model
attr_accessor :reports, :smokereports
belongs_to :reports, :smokereports, :reportlinks
end
The error I'm getting is undefined method `belongs_to' for Report:Class and a < top (required) > error. There's no database involved. I'm just trying to make it known that I want to click on any of the links and that filters only the block of content within that if/else statement.
Do I need to create a new controller for the if/else statement to work? Please let me know if more code is needed to get a better understanding. Thanks.
belongs_to is defined in ActiveRecord::Associations, which is part of ActiveRecord. You are manually including ActiveModel::Model which doesn't offer any association-related capabilities.
Includes the required interface for an object to interact with ActionPack, using different ActiveModel modules. It includes model name introspections, conversions, translations and validations. Besides that, it allows you to initialize the object with a hash of attributes, pretty much like ActiveRecord does.
Assuming you don't need the whole ActiveRecord database persistence capabilities but you need the association style, then you'll need to deal with that manually.
Therefore, you will have to define your own methods to append/remove associated records and keep track of them.
The association feature of ActiveRecord is strictly tied with the database persistence.
First let me explain an example:
In Model:
class Product < ActiveRecord::Base
has_many :line_items
def income
self.line_items.sum(:price)
end
def cost
self.line_items.sum(:cost)
end
def profit
self.income - self.cost
end
end
Then in Controller:
def show
#products = Product.all
end
And in View:
<% #products.each do |product| %>
Product Name: <%= product.name %>
Product Income: <%= product.income %>
Product Cost: <%= product.cost %>
Product Profit: <%= product.profit %>
<% end %>
Is it a good practice to call model methods from view?
When I searched for that, I found many people saying it is NOT a good practice to ever call model methods or access DB from views.
And on the other hand, some others said that don't call class methods or any method updates the DB from view but you can access any method that only retrieve data.
Then, is this code a good practice?
Its perfectly fine to call the object-methods/attributes from the view, as long as the call would not change the data. I mean, call readers/getters. A Bad practice would be to call/invoke methods that update/delete the data. Don't call setters.
Also, if there is any complex computation involved, resort to helpers.
Since your methods need to access line_items association, to avoid N+1 problem and calling DB queries from view, I'd advice fetching your line_items in show action, with includes:
def show
#products = Product.includes(:line_items)
end
With this adjustment, I think it's ok to call these methods in view.
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
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
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.