Efficiently query comments of all posts at a controller level? - ruby-on-rails

I have an index action in posts controller, and the view displays all posts with their comments following each post. My current implementation is, in the embedded html file, in the block of #posts.each do |p| I'm using p.comments to get all comments of each post. However, it seems not good to do this just in the view, but I can't find a way of doing that in controller. Can anyone give some suggestions? Or is this just the correct way to do?

In you controller, when you query posts, use includes(:comments) to reduce the number of SQL query from 1+n to 2:
#posts = Post.includes(:comments).where(......)
You can leave the code in your view as it is, p.comments won't fire more SQL queries.
Check the tutorial Eager loading associations.

Arie is spot on with solving the n + 1 problem. In terms of the view, you can still use the same approach, but it would be better to use collection partials:
# posts/index.html.erb
<%= render #posts %>
This will look for a partial located at /views/posts/_post.html.erb as send it a post variable. It will do this for every Post model in the #posts array.
For the comments, in _post.html.erb you could write:
# /views/posts/_post.html.erb
<% # Post content here %>
<%= render post.comments %>
Which like the posts collection, will render each comment using the partial located at /views/comments/_comment.html.erb
For more details, check out the rails guide

Related

Rails adds extra empty element to end of ordered list when iterating through array

I have a list of 4 comments I want to iterate into an ordered list to create 4 lines of comments.
My ERB partial:
<ol>
<% #post.comments.each do |c| %>
<li><%= c.body %></li>
<% end %>
</ol>
However, when it is being rendered on the page it looks as follows:
1. Foo
2. Bar
3. Baz
4. Foobar
5.
It seems to be adding an extra empty element at the end of the loop cycle.
What seems to be the issue with how I'm iterating through the array?
Please check how many comments you have associated with the post like below,
#post.comments.count
There will be 5 records. Delete the last one and add validation in comment model by below,
validates_presence_of :body
That should keep it from producing blank records. Let me know if that is not the case.
After some digging, I found out my answer.
I failed to mention in my question that I also had a form_for a new comment on the same page that listed comments.
On my Post controller, I had it set up as #comment = #post.comments.new on that action. Which, in turn created an empty, unsaved comment on that view for #post. So, on that action I changed it to a generic #comment = Comment.new and then manually passed in the ids that I needed to make the association in the end. I don't know if that's the "best" way, but it is the way it worked for me.

How to use pagination in rails 4.2.5 and also have the ability to reorder the list with ajax?

I'm working on an forum-type app in Rails v 4.2.5. My index page is a list of all the questions being discussed in the application and they are default sorted by the created_at date. I am also using the Kaminari gem to paginate all of the questions (25 per page). I originally had my app set up like this:
Questions Controller:
def index
#questions = Question.order(:created_at).page params[:page]
end
Index View:
# I render a partial that iterates through the questions list to display
# the title of the questions, then I include the paginate code below.
<div class="pagination">
<%= paginate #questions %>
</div>
I eventually decided I wanted users to be able to sort the questions by different criteria (e.g., by total amount of upvotes, by total amount of responses for a question, and by recently asked questions). Right now, you can click a link corresponding to the type of sort you want and it will AJAX the new sorted list (a partial) onto the page. However, when I do this, the pagination does not work and when I click to see the second page of the results, everything becomes unsorted.
Index View with Sort Links:
<div class="sort_selection">
<h3> Sort By: </h3>
<%= link_to "By Upvotes", "/questions/top?sort=votes", class: "question_sort_link" %>
<%= link_to "Answers Provided", "/questions/top?sort=answers", class: "question_sort_link" %>
<%= link_to "Recently Asked", "/questions/top?sort=recent", class: "question_sort_link" %>
</div>
Index Controller:
def top
case params[:sort]
when "votes"
#questions = Question.sort_by_votes #sort_by_votes is a method in my Question model that performs a SQL query
when "answers"
#questions = Question.where.not(answers_count: nil).order(answers_count: :desc).limit(25)
when "recent"
#questions = Question.order(created_at: :desc).limit(25)
end
render partial: 'questions_list', layout: false
end
Javascript AJAX
$(document).on("click", ".question_sort_link", function(event){
event.preventDefault();
$.ajax({
method: "get",
url: $(this).attr("href")
}).done(function(sorted){
$('.questions_show_sorted').replaceWith(sorted);
});
});
I fooled around with the placement of the <%= paginate #questions %> in the view, as well as removed the 25 limit in my controller and added .page params[:page] after all of the queries in the Top route but I still cannot get the pagination to work after I've AJAX'ed a sorted list onto the page. Does anyone have any suggestions?
When you are switching pages the data about sorting is lost, because you are reloading site with different parameters. You can either try to pass this data (the column you are going to sort, and info is it asc or desc) to the new page, and sort it before loading, or paginate it using AJAX (but that means loading everything at the first load). I can't tell about the "pagination does not work problem", because I don't know what you mean.
In general, this thing you are trying to do is rather complicated, and there is no simple solution for that. There is a library for JS called Datatables that (in theory) makes it easier. There is another library called jQ-Bootgrid, and a ruby gem called "Smart listing".
I think you need to provide the pagination links in your ajax response and replace them in your javascript callback.
I assume that you return html rather than json, which will make this a bit awkward. Perhaps you could build up a json response with pagination links and html content
{
next: /list?page=3,
prev: /list?page=1,
content: "<ul>
<li>foo</li>
<li>bar</li>
</ul>"
}

Rails render partial in a loop

I am new to rails and I am trying to render a partial within a loop as such.
Here , books is an array eager loaded in controller.
books.each do |book|
<%= render 'books/book', :book => book %>
end
This works fine. But when the books array is really huge, it takes time for the view to load completely. Is there any other efficient way using which the same can be achieved?. I tried partial with collection(like here) as well, but even that did not make much difference in the load time of books. Any suggestions would be highly appreciated. Thanks.
Rendering partial in a loop consume too much time because of open/close partial file every iteration. Instead of partial try to use your own helper for this purpose.
If you have list of values to display in the same view then you can iterate the values in the same view instead of rendering a new partial each time. If it's not the case then it is better to pass values from controller to your view. Hope it helps.
How about using "proc" on your view top, and then call it in your loop.
<% book_proc = proc do |book| %>
#your html call
<% nil %><%# return nil to prevent print out in last string %>
<% end %>
<% books.each do |book| %>
<%= book_proc.call(book) %>
<% end %>
You write that you're new to ruby/rails, so have you ever tried using
pagination
to solve your performance-problem?
A pagination will split your list into parts of e.g. 20 books and sets a paginator mostly at the bottom of your table.
I recently worked on a project in which I needed to render a table with about 1000 rows and I obviously experienced performance issues.
When pagination cannot be applied (due to requirements) and speed is required, then helpers is the solution (as already answered by Dima Melnik).
To prove what i said, i give a link to a performance test produced by Ben Scofield:
http://viget.com/extend/helpers-vs-partials-a-performance-question

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 to make the view simpler, the controller more useful?

This question relates to cleaning up the view and giving the controller more of the work.
I have many cases in my project where I have nested variables being displayed in my view. For example:
# controller
#customers = Customer.find_all_by_active(true)
render :layout => 'forms'
# view
<% #customers.each do |c| %>
<%= c.name %>
<% #orders = c.orders %> # I often end up defining nested variables inside the view
<% #orders.each do |o| %>
...
<% end %>
<% end %>
I am fairly new to RoR but it seems that what I'm doing here is at odds with the 'intelligent controller, dumb view' mentality. Where I have many customers, each with many orders, how can I define these variables properly inside my controller and then access them inside the view?
If you could provide an example of how the controller would look and then how I would relate to that in the view it would be incredibly helpful. Thank you very much!
I don't think there is anything drastically wrong with what you're doing. Looping through the customers and outputting some of their attributes and for each customer, looping through their orders and outputting some attributes is very much a view-oriented operation.
In the MVC architecture, the controller has responsibility for interacting with the model, selecting the view and (certainly in the case of Rails) providing the view with the information it needs to render the model.
You might consider extracting the code into a view helper though, if you have that exact code repeated more than once. You could even genericize it, passing in the name of a model and association. I haven't tested it, but you should be able to do something like this:
def display_attributes(models, association, attribute, association_attribute)
content = ''
models.each do |m|
content << "<p>#{m.attribute}</p>"
associated_models = m.association
associated_models.each do |am|
content << "<p>#{am.association_attribute}</p>"
end
end
content
end
Then in the view, you could use the helper like this:
<%= display_attributes(#customers, orders, name, name) %>
Obviously you would change the HTML markup within the helper method to suit your requirements. Note that if you're not using Rails 3 then you'll want to escape the output of the attribute names in the helper method.
I don't think there's anything wrong with your code. I'd just suggest for you to use a :include in your find
#customers = Customer.find_all_by_active(true, :include => :orders)
to reduce the number of queries.
I see nothing wrong with the code as you showed.
You are mixed up about the "intelligent controller, dumb view" approach though, i tend to prefer the "skinny controller, fat model", so indeed the view should be dumb, but you put the intelligence inside your model, and your helpers (or use a presenter), but definitely not in the controller.

Resources